Fix lint warnings and code cleanup
- Remove duplicate calc structs, use model.CalcXxx - Replace log.Printf with logger in recommend handler - Remove dead code (unused var, dummy imports) - Clean up response helpers - Add api/ package with route registration Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
94895cbc22
commit
cb7e595c27
87
internal/api/router.go
Normal file
87
internal/api/router.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"floorvisualizer/internal/handler"
|
||||||
|
"floorvisualizer/internal/middleware"
|
||||||
|
"floorvisualizer/internal/model"
|
||||||
|
"floorvisualizer/internal/openrouter"
|
||||||
|
"floorvisualizer/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterRoutes sets up all HTTP routes with injected dependencies.
|
||||||
|
func RegisterRoutes(mux *http.ServeMux, client *openrouter.Client, db *sql.DB, authSvc *service.AuthService) {
|
||||||
|
auth := middleware.RequireAuth(authSvc)
|
||||||
|
|
||||||
|
// Pre-build handlers — dependencies injected once
|
||||||
|
var (
|
||||||
|
register = handler.Register(authSvc, db)
|
||||||
|
login = handler.Login(authSvc, db)
|
||||||
|
me = auth(handler.Me(db))
|
||||||
|
uploadAvatar = auth(handler.UploadAvatar(db))
|
||||||
|
favorites = auth(handler.Favorites(db))
|
||||||
|
favoritesSub = auth(handler.Favorites(db))
|
||||||
|
projects = auth(handler.Projects(db))
|
||||||
|
projectsSub = auth(handler.Projects(db))
|
||||||
|
frequentBrands = auth(handler.FrequentBrands(db))
|
||||||
|
products = handler.Products(db)
|
||||||
|
productsSub = handler.Products(db)
|
||||||
|
productOpts = handler.ProductOptions(db)
|
||||||
|
filterOpts = handler.FilterOptions(db)
|
||||||
|
recommend = handler.Recommend(db)
|
||||||
|
floorGenerate = handler.FloorGenerate(client, db)
|
||||||
|
floorOptions = handler.FloorOptions(db)
|
||||||
|
articleRecommend = handler.ArticleRecommend(db)
|
||||||
|
articles = handler.Articles(db)
|
||||||
|
articlesSub = handler.Articles(db)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Route mapping
|
||||||
|
mux.HandleFunc("/auth/register", register)
|
||||||
|
mux.HandleFunc("/auth/login", login)
|
||||||
|
mux.HandleFunc("/user/me", me)
|
||||||
|
mux.HandleFunc("/user/avatar", uploadAvatar)
|
||||||
|
mux.HandleFunc("/user/favorites", favorites)
|
||||||
|
mux.HandleFunc("/user/favorites/", favoritesSub)
|
||||||
|
mux.HandleFunc("/user/projects", projects)
|
||||||
|
mux.HandleFunc("/user/projects/", projectsSub)
|
||||||
|
mux.HandleFunc("/user/frequent-brands", frequentBrands)
|
||||||
|
mux.HandleFunc("/healthz", handler.Healthz)
|
||||||
|
mux.HandleFunc("/upload", handler.Upload)
|
||||||
|
mux.HandleFunc("/products", products)
|
||||||
|
mux.HandleFunc("/products/", productsSub)
|
||||||
|
mux.HandleFunc("/product-options", productOpts)
|
||||||
|
mux.HandleFunc("/filter-options", filterOpts)
|
||||||
|
mux.HandleFunc("/recommend/api", recommend)
|
||||||
|
mux.HandleFunc("/calculator/calc", handler.Calc)
|
||||||
|
mux.HandleFunc("/floor/generate", floorGenerate)
|
||||||
|
mux.HandleFunc("/floor/status", handler.FloorStatus)
|
||||||
|
mux.HandleFunc("/floor/options", floorOptions)
|
||||||
|
mux.HandleFunc("/articles/recommend", articleRecommend)
|
||||||
|
mux.HandleFunc("/articles", articles)
|
||||||
|
mux.HandleFunc("/articles/", articlesSub)
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
mux.Handle("/outputs/", http.StripPrefix("/outputs/", http.FileServer(http.Dir("./outputs"))))
|
||||||
|
mux.Handle("/uploads/", http.StripPrefix("/uploads/", http.FileServer(http.Dir("./uploads"))))
|
||||||
|
mux.Handle("/knowledge_images/", http.StripPrefix("/knowledge_images/", http.FileServer(http.Dir("./knowledge_images"))))
|
||||||
|
mux.Handle("/images/knowledge/", http.StripPrefix("/images/knowledge/", http.FileServer(http.Dir("./knowledge_images"))))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Response helpers ──────────────────────────────────────
|
||||||
|
|
||||||
|
// JSON writes a success response with the API envelope.
|
||||||
|
func JSON(w http.ResponseWriter, status int, v any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
json.NewEncoder(w).Encode(model.ApiResponse{Code: status, Data: v})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(w http.ResponseWriter, status int, msg string) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
json.NewEncoder(w).Encode(model.ApiError{Code: status, Error: msg})
|
||||||
|
}
|
||||||
15
internal/handler/api.go
Normal file
15
internal/handler/api.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"floorvisualizer/internal/openrouter"
|
||||||
|
"floorvisualizer/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// API holds all handler dependencies, injected once at startup.
|
||||||
|
type API struct {
|
||||||
|
DB *sql.DB
|
||||||
|
Client *openrouter.Client
|
||||||
|
AuthSvc *service.AuthService
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"floorvisualizer/internal/repository"
|
"floorvisualizer/internal/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleArticles(db *sql.DB) http.HandlerFunc {
|
func Articles(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// /articles/{id} — detail
|
// /articles/{id} — detail
|
||||||
idStr := strings.TrimPrefix(r.URL.Path, "/articles/")
|
idStr := strings.TrimPrefix(r.URL.Path, "/articles/")
|
||||||
@ -56,7 +56,7 @@ func handleArticles(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleArticleRecommend(db *sql.DB) http.HandlerFunc {
|
func ArticleRecommend(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
count := 6
|
count := 6
|
||||||
if c, err := strconv.Atoi(r.URL.Query().Get("count")); err == nil && c > 0 && c <= 20 {
|
if c, err := strconv.Atoi(r.URL.Query().Get("count")); err == nil && c > 0 && c <= 20 {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
"floorvisualizer/internal/service"
|
"floorvisualizer/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleRegister(authSvc *service.AuthService, db *sql.DB) http.HandlerFunc {
|
func Register(authSvc *service.AuthService, db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeErr(w, 405, "Method not allowed")
|
writeErr(w, 405, "Method not allowed")
|
||||||
@ -28,7 +28,7 @@ func handleRegister(authSvc *service.AuthService, db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLogin(authSvc *service.AuthService, db *sql.DB) http.HandlerFunc {
|
func Login(authSvc *service.AuthService, db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeErr(w, 405, "Method not allowed")
|
writeErr(w, 405, "Method not allowed")
|
||||||
|
|||||||
@ -4,68 +4,10 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"floorvisualizer/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Calculator models ───────────────────────────────────────
|
|
||||||
|
|
||||||
type CalcRoomInput struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Length float64 `json:"length"`
|
|
||||||
Width float64 `json:"width"`
|
|
||||||
Deductions []CalcDeduction `json:"deductions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CalcDeduction struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Shape string `json:"shape"` // rectangle, circle, custom
|
|
||||||
Length float64 `json:"length"`
|
|
||||||
Width float64 `json:"width"`
|
|
||||||
Radius float64 `json:"radius"`
|
|
||||||
Area float64 `json:"area"` // custom area override
|
|
||||||
}
|
|
||||||
|
|
||||||
type CalcInput struct {
|
|
||||||
FloorType string `json:"floor_type"` // wood, tile
|
|
||||||
Rooms []CalcRoomInput `json:"rooms"`
|
|
||||||
PatternIndex int `json:"pattern_index"`
|
|
||||||
PieceLength float64 `json:"piece_length"`
|
|
||||||
PieceWidth float64 `json:"piece_width"`
|
|
||||||
PiecesPerBox int `json:"pieces_per_box"`
|
|
||||||
PricePerSqm float64 `json:"price_per_sqm"`
|
|
||||||
JointWidth float64 `json:"joint_width"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CalcRoomResult struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Length float64 `json:"length"`
|
|
||||||
Width float64 `json:"width"`
|
|
||||||
Area float64 `json:"area"`
|
|
||||||
Deductions []CalcDeduction `json:"deductions"`
|
|
||||||
DeductionTotal float64 `json:"deduction_total"`
|
|
||||||
NetArea float64 `json:"net_area"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CalcResult struct {
|
|
||||||
FloorType string `json:"floor_type"`
|
|
||||||
Rooms []CalcRoomResult `json:"rooms"`
|
|
||||||
TotalRoomArea float64 `json:"total_room_area"`
|
|
||||||
TotalDeduct float64 `json:"total_deduct"`
|
|
||||||
TotalNetArea float64 `json:"total_net_area"`
|
|
||||||
WasteRate float64 `json:"waste_rate"`
|
|
||||||
WasteArea float64 `json:"waste_area"`
|
|
||||||
TotalArea float64 `json:"total_area"`
|
|
||||||
PiecesNeeded int `json:"pieces_needed"`
|
|
||||||
BoxesNeeded *float64 `json:"boxes_needed,omitempty"`
|
|
||||||
JointWidth float64 `json:"joint_width"`
|
|
||||||
GroutKg *float64 `json:"grout_kg,omitempty"`
|
|
||||||
GroutBags *int `json:"grout_bags,omitempty"`
|
|
||||||
PricePerSqm float64 `json:"price_per_sqm"`
|
|
||||||
TotalPrice *float64 `json:"total_price,omitempty"`
|
|
||||||
PatternName string `json:"pattern_name"`
|
|
||||||
PieceLength float64 `json:"piece_length"`
|
|
||||||
PieceWidth float64 `json:"piece_width"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var woodPatternsCalc = []struct {
|
var woodPatternsCalc = []struct {
|
||||||
Name string
|
Name string
|
||||||
Waste float64
|
Waste float64
|
||||||
@ -90,13 +32,13 @@ var tilePatternsCalc = []struct {
|
|||||||
|
|
||||||
// ── Handler ─────────────────────────────────────────────────
|
// ── Handler ─────────────────────────────────────────────────
|
||||||
|
|
||||||
func handleCalc(w http.ResponseWriter, r *http.Request) {
|
func Calc(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeErr(w, 405, "Method not allowed")
|
writeErr(w, 405, "Method not allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var input CalcInput
|
var input model.CalcInput
|
||||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||||
writeJSON(w, 400, map[string]string{"error": "invalid JSON: " + err.Error()})
|
writeJSON(w, 400, map[string]string{"error": "invalid JSON: " + err.Error()})
|
||||||
return
|
return
|
||||||
@ -117,7 +59,7 @@ func handleCalc(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// ── Calculation ─────────────────────────────────────────────
|
// ── Calculation ─────────────────────────────────────────────
|
||||||
|
|
||||||
func calculate(input CalcInput) CalcResult {
|
func calculate(input model.CalcInput) model.CalcResult {
|
||||||
patterns := woodPatternsCalc
|
patterns := woodPatternsCalc
|
||||||
if input.FloorType == "tile" {
|
if input.FloorType == "tile" {
|
||||||
patterns = tilePatternsCalc
|
patterns = tilePatternsCalc
|
||||||
@ -127,14 +69,14 @@ func calculate(input CalcInput) CalcResult {
|
|||||||
}
|
}
|
||||||
wasteRate := patterns[input.PatternIndex].Waste
|
wasteRate := patterns[input.PatternIndex].Waste
|
||||||
|
|
||||||
var roomResults []CalcRoomResult
|
var roomResults []model.CalcRoomResult
|
||||||
totalRoomArea := 0.0
|
totalRoomArea := 0.0
|
||||||
totalDeduct := 0.0
|
totalDeduct := 0.0
|
||||||
|
|
||||||
for _, room := range input.Rooms {
|
for _, room := range input.Rooms {
|
||||||
area := room.Length * room.Width
|
area := room.Length * room.Width
|
||||||
deductTotal := 0.0
|
deductTotal := 0.0
|
||||||
var dedResults []CalcDeduction
|
var dedResults []model.CalcDeduction
|
||||||
|
|
||||||
for _, d := range room.Deductions {
|
for _, d := range room.Deductions {
|
||||||
dedArea := deductionArea(d)
|
dedArea := deductionArea(d)
|
||||||
@ -144,9 +86,11 @@ func calculate(input CalcInput) CalcResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
netArea := area - deductTotal
|
netArea := area - deductTotal
|
||||||
if netArea < 0 { netArea = 0 }
|
if netArea < 0 {
|
||||||
|
netArea = 0
|
||||||
|
}
|
||||||
|
|
||||||
roomResults = append(roomResults, CalcRoomResult{
|
roomResults = append(roomResults, model.CalcRoomResult{
|
||||||
Name: room.Name, Length: room.Length, Width: room.Width,
|
Name: room.Name, Length: room.Length, Width: room.Width,
|
||||||
Area: round2(area), Deductions: dedResults,
|
Area: round2(area), Deductions: dedResults,
|
||||||
DeductionTotal: round2(deductTotal), NetArea: round2(netArea),
|
DeductionTotal: round2(deductTotal), NetArea: round2(netArea),
|
||||||
@ -156,7 +100,9 @@ func calculate(input CalcInput) CalcResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
totalNetArea := totalRoomArea - totalDeduct
|
totalNetArea := totalRoomArea - totalDeduct
|
||||||
if totalNetArea < 0 { totalNetArea = 0 }
|
if totalNetArea < 0 {
|
||||||
|
totalNetArea = 0
|
||||||
|
}
|
||||||
|
|
||||||
// Adjust piece for joint
|
// Adjust piece for joint
|
||||||
effLength := input.PieceLength
|
effLength := input.PieceLength
|
||||||
@ -167,14 +113,16 @@ func calculate(input CalcInput) CalcResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pieceArea := effLength * effWidth
|
pieceArea := effLength * effWidth
|
||||||
if pieceArea <= 0 { pieceArea = 1 }
|
if pieceArea <= 0 {
|
||||||
|
pieceArea = 1
|
||||||
|
}
|
||||||
|
|
||||||
totalPieces := totalNetArea / pieceArea
|
totalPieces := totalNetArea / pieceArea
|
||||||
wasteArea := totalNetArea * wasteRate
|
wasteArea := totalNetArea * wasteRate
|
||||||
totalArea := totalNetArea + wasteArea
|
totalArea := totalNetArea + wasteArea
|
||||||
piecesNeeded := int(math.Ceil(totalArea / pieceArea))
|
piecesNeeded := int(math.Ceil(totalArea / pieceArea))
|
||||||
|
|
||||||
result := CalcResult{
|
result := model.CalcResult{
|
||||||
FloorType: input.FloorType,
|
FloorType: input.FloorType,
|
||||||
Rooms: roomResults,
|
Rooms: roomResults,
|
||||||
TotalRoomArea: round2(totalRoomArea),
|
TotalRoomArea: round2(totalRoomArea),
|
||||||
@ -214,7 +162,7 @@ func calculate(input CalcInput) CalcResult {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func deductionArea(d CalcDeduction) float64 {
|
func deductionArea(d model.CalcDeduction) float64 {
|
||||||
switch d.Shape {
|
switch d.Shape {
|
||||||
case "circle":
|
case "circle":
|
||||||
return math.Pi * d.Radius * d.Radius
|
return math.Pi * d.Radius * d.Radius
|
||||||
|
|||||||
@ -131,13 +131,19 @@ func findFloorBySKU(db *sql.DB, sku string) *FloorOption {
|
|||||||
|
|
||||||
func FindFloorBySKUPublic(db *sql.DB, sku string) *FloorOption { return findFloorBySKU(db, sku) }
|
func FindFloorBySKUPublic(db *sql.DB, sku string) *FloorOption { return findFloorBySKU(db, sku) }
|
||||||
func FindPatternByCodePublic(code string) *PatternOption {
|
func FindPatternByCodePublic(code string) *PatternOption {
|
||||||
c := 0; fmt.Sscanf(code, "%d", &c); return findPatternByCode(c)
|
c := 0
|
||||||
|
fmt.Sscanf(code, "%d", &c)
|
||||||
|
return findPatternByCode(c)
|
||||||
}
|
}
|
||||||
func FindRoomByCodePublic(code string) *RoomOption {
|
func FindRoomByCodePublic(code string) *RoomOption {
|
||||||
c := 0; fmt.Sscanf(code, "%d", &c); return findRoomByCode(c)
|
c := 0
|
||||||
|
fmt.Sscanf(code, "%d", &c)
|
||||||
|
return findRoomByCode(c)
|
||||||
|
}
|
||||||
|
func BuildMaskPromptPublic(room *RoomOption) string { return buildMaskPrompt(room) }
|
||||||
|
func BuildFloorPromptPublic(f FloorOption, p PatternOption, room *RoomOption) string {
|
||||||
|
return buildFloorPrompt(f, p, room)
|
||||||
}
|
}
|
||||||
func BuildMaskPromptPublic(room *RoomOption) string { return buildMaskPrompt(room) }
|
|
||||||
func BuildFloorPromptPublic(f FloorOption, p PatternOption, room *RoomOption) string { return buildFloorPrompt(f, p, room) }
|
|
||||||
|
|
||||||
// ── Floor Generate Handler (Redis queue) ──────────────────
|
// ── Floor Generate Handler (Redis queue) ──────────────────
|
||||||
|
|
||||||
@ -162,7 +168,7 @@ type JobStatusResp struct {
|
|||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleFloorGenerate(client *openrouter.Client, db *sql.DB) http.HandlerFunc {
|
func FloorGenerate(client *openrouter.Client, db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeErr(w, 405, "Method not allowed")
|
writeErr(w, 405, "Method not allowed")
|
||||||
@ -223,11 +229,12 @@ func handleFloorGenerate(client *openrouter.Client, db *sql.DB) http.HandlerFunc
|
|||||||
writeJSON(w, 200, map[string]string{"job_id": jobID})
|
writeJSON(w, 200, map[string]string{"job_id": jobID})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Progress ───────────────────────────────────────────────
|
// ── Progress ───────────────────────────────────────────────
|
||||||
|
|
||||||
// ── Floor Status Query ─────────────────────────────────
|
// ── Floor Status Query ─────────────────────────────────
|
||||||
|
|
||||||
func handleFloorStatus(w http.ResponseWriter, r *http.Request) {
|
func FloorStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
jobID := r.URL.Query().Get("job_id")
|
jobID := r.URL.Query().Get("job_id")
|
||||||
if jobID == "" {
|
if jobID == "" {
|
||||||
writeErr(w, 400, "job_id is required")
|
writeErr(w, 400, "job_id is required")
|
||||||
@ -244,9 +251,10 @@ func handleFloorStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
writeJSON(w, 200, js)
|
writeJSON(w, 200, js)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Options ────────────────────────────────────────────────
|
// ── Options ────────────────────────────────────────────────
|
||||||
|
|
||||||
func handleFloorOptions(db *sql.DB) http.HandlerFunc {
|
func FloorOptions(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
woodFloors, _ := loadFloorOptions(db, "Hardwood")
|
woodFloors, _ := loadFloorOptions(db, "Hardwood")
|
||||||
woodFloors2, _ := loadFloorOptions(db, "Engineered Wood")
|
woodFloors2, _ := loadFloorOptions(db, "Engineered Wood")
|
||||||
@ -308,7 +316,7 @@ Style: photorealistic, soft natural matte finish, minimal reflections, no mirror
|
|||||||
|
|
||||||
// ── Content Check ─────────────────────────────────────────
|
// ── Content Check ─────────────────────────────────────────
|
||||||
|
|
||||||
func handleCheckContent(client *openrouter.Client) http.HandlerFunc {
|
func CheckContent(client *openrouter.Client) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeErr(w, 405, "Method not allowed")
|
writeErr(w, 405, "Method not allowed")
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleHealthz(w http.ResponseWriter, r *http.Request) {
|
func Healthz(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSON(w, 200, map[string]string{
|
writeJSON(w, 200, map[string]string{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
})
|
})
|
||||||
|
|||||||
@ -31,11 +31,11 @@ type ProductItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProductListResponse struct {
|
type ProductListResponse struct {
|
||||||
Products []ProductItem `json:"products"`
|
Products []ProductItem `json:"products"`
|
||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
Limit int `json:"limit"`
|
Limit int `json:"limit"`
|
||||||
TotalPages int `json:"total_pages"`
|
TotalPages int `json:"total_pages"`
|
||||||
FilterOptions *repository.FilterOptions `json:"filter_options"`
|
FilterOptions *repository.FilterOptions `json:"filter_options"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ func SetProductsCache(
|
|||||||
productsTrackView = tracker
|
productsTrackView = tracker
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleProducts(db *sql.DB) http.HandlerFunc {
|
func Products(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Detail: /products/{sku}
|
// Detail: /products/{sku}
|
||||||
sku := strings.TrimPrefix(r.URL.Path, "/products/")
|
sku := strings.TrimPrefix(r.URL.Path, "/products/")
|
||||||
@ -69,8 +69,7 @@ func handleProducts(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if product == nil {
|
if product == nil {
|
||||||
var err error
|
product, err := repository.GetProductBySKU(db, sku)
|
||||||
product, err = repository.GetProductBySKU(db, sku)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSON(w, 404, map[string]string{"error": "product not found"})
|
writeJSON(w, 404, map[string]string{"error": "product not found"})
|
||||||
return
|
return
|
||||||
@ -152,8 +151,8 @@ func handleProducts(db *sql.DB) http.HandlerFunc {
|
|||||||
PricePerSqft: p.PricePerSqft, PriceTier: p.PriceTier,
|
PricePerSqft: p.PricePerSqft, PriceTier: p.PriceTier,
|
||||||
MainImageURL: p.MainImageURL, SizeLabel: p.SizeLabel,
|
MainImageURL: p.MainImageURL, SizeLabel: p.SizeLabel,
|
||||||
CoverageSqftPerBox: p.CoverageSqftPerBox,
|
CoverageSqftPerBox: p.CoverageSqftPerBox,
|
||||||
IsFavorited: favSKUs[p.SKU],
|
IsFavorited: favSKUs[p.SKU],
|
||||||
SpecCount: specCounts[p.Brand+"|"+p.StyleName+"|"+p.SeriesName],
|
SpecCount: specCounts[p.Brand+"|"+p.StyleName+"|"+p.SeriesName],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +172,7 @@ func handleProducts(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleProductOptions(db *sql.DB) http.HandlerFunc {
|
func ProductOptions(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
opts, err := repository.GetFilterOptions(db)
|
opts, err := repository.GetFilterOptions(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -184,7 +183,7 @@ func handleProductOptions(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleFilterOptions(db *sql.DB) http.HandlerFunc {
|
func FilterOptions(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
filters, err := repository.GetFilterHierarchy(db)
|
filters, err := repository.GetFilterHierarchy(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"floorvisualizer/internal/logger"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"floorvisualizer/internal/model"
|
"floorvisualizer/internal/model"
|
||||||
@ -21,17 +21,19 @@ type RecommendResponse struct {
|
|||||||
var productService *service.ProductService
|
var productService *service.ProductService
|
||||||
|
|
||||||
func initProductService(db *sql.DB) {
|
func initProductService(db *sql.DB) {
|
||||||
if productService != nil { return }
|
if productService != nil {
|
||||||
products, err := repository.QueryAllProducts(db)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to load products: %v", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Loaded %d products for recommendation engine", len(products))
|
products, err := repository.QueryAllProducts(db)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Failed to load products: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("Loaded %d products for recommendation engine", len(products))
|
||||||
productService = service.NewProductService(products, model.DefaultEngineConfig())
|
productService = service.NewProductService(products, model.DefaultEngineConfig())
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRecommend(db *sql.DB) http.HandlerFunc {
|
func Recommend(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
sku := r.URL.Query().Get("sku")
|
sku := r.URL.Query().Get("sku")
|
||||||
if sku == "" {
|
if sku == "" {
|
||||||
|
|||||||
@ -1,15 +1,11 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"floorvisualizer/internal/middleware"
|
"floorvisualizer/internal/model"
|
||||||
"floorvisualizer/internal/openrouter"
|
|
||||||
"floorvisualizer/internal/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewProxyClient(proxyURLStr string) *http.Client {
|
func NewProxyClient(proxyURLStr string) *http.Client {
|
||||||
@ -20,57 +16,9 @@ func NewProxyClient(proxyURLStr string) *http.Client {
|
|||||||
return &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(u)}}
|
return &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(u)}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterAll(mux *http.ServeMux, client *openrouter.Client, db *sql.DB, authSvc *service.AuthService) {
|
|
||||||
requireAuth := middleware.RequireAuth(authSvc)
|
|
||||||
|
|
||||||
// Auth
|
|
||||||
mux.HandleFunc("/auth/register", handleRegister(authSvc, db))
|
|
||||||
mux.HandleFunc("/auth/login", handleLogin(authSvc, db))
|
|
||||||
|
|
||||||
// User (protected)
|
|
||||||
mux.HandleFunc("/user/me", requireAuth(handleMe(db)))
|
|
||||||
mux.HandleFunc("/user/avatar", requireAuth(handleUploadAvatar(db)))
|
|
||||||
mux.HandleFunc("/user/favorites", requireAuth(handleFavorites(db)))
|
|
||||||
mux.HandleFunc("/user/favorites/", requireAuth(handleFavorites(db)))
|
|
||||||
mux.HandleFunc("/user/projects", requireAuth(handleProjects(db)))
|
|
||||||
mux.HandleFunc("/user/projects/", requireAuth(handleProjects(db)))
|
|
||||||
mux.HandleFunc("/user/frequent-brands", requireAuth(handleFrequentBrands(db)))
|
|
||||||
|
|
||||||
// Upload
|
|
||||||
mux.HandleFunc("/healthz", handleHealthz)
|
|
||||||
mux.HandleFunc("/upload", handleUpload)
|
|
||||||
|
|
||||||
// Products
|
|
||||||
mux.HandleFunc("/products", handleProducts(db))
|
|
||||||
mux.HandleFunc("/products/", handleProducts(db))
|
|
||||||
mux.HandleFunc("/product-options", handleProductOptions(db))
|
|
||||||
mux.HandleFunc("/filter-options", handleFilterOptions(db))
|
|
||||||
|
|
||||||
// Recommend
|
|
||||||
mux.HandleFunc("/recommend/api", handleRecommend(db))
|
|
||||||
|
|
||||||
// Calculator
|
|
||||||
mux.HandleFunc("/calculator/calc", handleCalc)
|
|
||||||
|
|
||||||
// Floor AI
|
|
||||||
mux.HandleFunc("/floor/generate", handleFloorGenerate(client, db))
|
|
||||||
mux.HandleFunc("/floor/status", handleFloorStatus)
|
|
||||||
mux.HandleFunc("/floor/options", handleFloorOptions(db))
|
|
||||||
|
|
||||||
// Articles (recommend before /articles/ to avoid path conflict)
|
|
||||||
mux.HandleFunc("/articles/recommend", handleArticleRecommend(db))
|
|
||||||
mux.HandleFunc("/articles", handleArticles(db))
|
|
||||||
mux.HandleFunc("/articles/", handleArticles(db))
|
|
||||||
|
|
||||||
// Static
|
|
||||||
mux.Handle("/outputs/", http.StripPrefix("/outputs/", http.FileServer(http.Dir("./outputs"))))
|
|
||||||
mux.Handle("/uploads/", http.StripPrefix("/uploads/", http.FileServer(http.Dir("./uploads"))))
|
|
||||||
mux.Handle("/knowledge_images/", http.StripPrefix("/knowledge_images/", http.FileServer(http.Dir("./knowledge_images"))))
|
|
||||||
mux.Handle("/images/knowledge/", http.StripPrefix("/images/knowledge/", http.FileServer(http.Dir("./knowledge_images"))))
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
if status >= 400 {
|
if status >= 400 {
|
||||||
msg := "Unknown error"
|
msg := "Unknown error"
|
||||||
switch x := v.(type) {
|
switch x := v.(type) {
|
||||||
@ -80,19 +28,15 @@ func writeJSON(w http.ResponseWriter, status int, v any) {
|
|||||||
if e, ok := x["error"]; ok {
|
if e, ok := x["error"]; ok {
|
||||||
msg = e
|
msg = e
|
||||||
}
|
}
|
||||||
case map[string]any:
|
|
||||||
if e, ok := x["error"]; ok {
|
|
||||||
msg = fmt.Sprint(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
w.WriteHeader(status)
|
json.NewEncoder(w).Encode(model.ApiError{Code: status, Error: msg})
|
||||||
json.NewEncoder(w).Encode(map[string]any{"code": status, "error": msg})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(status)
|
json.NewEncoder(w).Encode(model.ApiResponse{Code: status, Data: v})
|
||||||
json.NewEncoder(w).Encode(map[string]any{"code": status, "data": v})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeErr(w http.ResponseWriter, status int, msg string) {
|
func writeErr(w http.ResponseWriter, status int, msg string) {
|
||||||
writeJSON(w, status, msg)
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
json.NewEncoder(w).Encode(model.ApiError{Code: status, Error: msg})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleUpload(w http.ResponseWriter, r *http.Request) {
|
func Upload(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeErr(w, 405, "Method not allowed")
|
writeErr(w, 405, "Method not allowed")
|
||||||
return
|
return
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import (
|
|||||||
"floorvisualizer/internal/repository"
|
"floorvisualizer/internal/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleMe(db *sql.DB) http.HandlerFunc {
|
func Me(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := middleware.UserID(r.Context())
|
userID := middleware.UserID(r.Context())
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
@ -50,7 +50,7 @@ func handleMe(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleFavorites(db *sql.DB) http.HandlerFunc {
|
func Favorites(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := middleware.UserID(r.Context())
|
userID := middleware.UserID(r.Context())
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
@ -103,7 +103,7 @@ func handleFavorites(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleProjects(db *sql.DB) http.HandlerFunc {
|
func Projects(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := middleware.UserID(r.Context())
|
userID := middleware.UserID(r.Context())
|
||||||
projectID := strings.TrimPrefix(r.URL.Path, "/user/projects/")
|
projectID := strings.TrimPrefix(r.URL.Path, "/user/projects/")
|
||||||
@ -161,7 +161,9 @@ func handleSingleProject(w http.ResponseWriter, r *http.Request, db *sql.DB, use
|
|||||||
}
|
}
|
||||||
writeJSON(w, 200, project)
|
writeJSON(w, 200, project)
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
var input struct{ Name string `json:"name"` }
|
var input struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||||
writeJSON(w, 400, map[string]string{"error": "invalid request body"})
|
writeJSON(w, 400, map[string]string{"error": "invalid request body"})
|
||||||
return
|
return
|
||||||
@ -183,7 +185,7 @@ func handleSingleProject(w http.ResponseWriter, r *http.Request, db *sql.DB, use
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUploadAvatar(db *sql.DB) http.HandlerFunc {
|
func UploadAvatar(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := middleware.UserID(r.Context())
|
userID := middleware.UserID(r.Context())
|
||||||
if err := r.ParseMultipartForm(5 << 20); err != nil {
|
if err := r.ParseMultipartForm(5 << 20); err != nil {
|
||||||
@ -233,10 +235,9 @@ func handleUploadAvatar(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── Brand view tracking ───────────────────────────────────
|
// ── Brand view tracking ───────────────────────────────────
|
||||||
|
|
||||||
func handleBrandView(db *sql.DB) http.HandlerFunc {
|
func BrandView(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeErr(w, 405, "Method not allowed")
|
writeErr(w, 405, "Method not allowed")
|
||||||
@ -268,7 +269,7 @@ func handleBrandView(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleFrequentBrands(db *sql.DB) http.HandlerFunc {
|
func FrequentBrands(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := middleware.UserID(r.Context())
|
userID := middleware.UserID(r.Context())
|
||||||
if userID == "" {
|
if userID == "" {
|
||||||
|
|||||||
63
internal/model/calc.go
Normal file
63
internal/model/calc.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// ── Calculator input ──────────────────────────────────────
|
||||||
|
|
||||||
|
type CalcRoomInput struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Length float64 `json:"length"`
|
||||||
|
Width float64 `json:"width"`
|
||||||
|
Deductions []CalcDeduction `json:"deductions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CalcDeduction struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Shape string `json:"shape"` // rectangle, circle, custom
|
||||||
|
Length float64 `json:"length"`
|
||||||
|
Width float64 `json:"width"`
|
||||||
|
Radius float64 `json:"radius"`
|
||||||
|
Area float64 `json:"area"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CalcInput struct {
|
||||||
|
FloorType string `json:"floor_type"`
|
||||||
|
Rooms []CalcRoomInput `json:"rooms"`
|
||||||
|
PatternIndex int `json:"pattern_index"`
|
||||||
|
PieceLength float64 `json:"piece_length"`
|
||||||
|
PieceWidth float64 `json:"piece_width"`
|
||||||
|
PiecesPerBox int `json:"pieces_per_box"`
|
||||||
|
PricePerSqm float64 `json:"price_per_sqm"`
|
||||||
|
JointWidth float64 `json:"joint_width"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Calculator output ─────────────────────────────────────
|
||||||
|
|
||||||
|
type CalcRoomResult struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Length float64 `json:"length"`
|
||||||
|
Width float64 `json:"width"`
|
||||||
|
Area float64 `json:"area"`
|
||||||
|
Deductions []CalcDeduction `json:"deductions"`
|
||||||
|
DeductionTotal float64 `json:"deduction_total"`
|
||||||
|
NetArea float64 `json:"net_area"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CalcResult struct {
|
||||||
|
FloorType string `json:"floor_type"`
|
||||||
|
Rooms []CalcRoomResult `json:"rooms"`
|
||||||
|
TotalRoomArea float64 `json:"total_room_area"`
|
||||||
|
TotalDeduct float64 `json:"total_deduct"`
|
||||||
|
TotalNetArea float64 `json:"total_net_area"`
|
||||||
|
WasteRate float64 `json:"waste_rate"`
|
||||||
|
WasteArea float64 `json:"waste_area"`
|
||||||
|
TotalArea float64 `json:"total_area"`
|
||||||
|
PiecesNeeded int `json:"pieces_needed"`
|
||||||
|
BoxesNeeded *float64 `json:"boxes_needed,omitempty"`
|
||||||
|
JointWidth float64 `json:"joint_width"`
|
||||||
|
GroutKg *float64 `json:"grout_kg,omitempty"`
|
||||||
|
GroutBags *int `json:"grout_bags,omitempty"`
|
||||||
|
PricePerSqm float64 `json:"price_per_sqm"`
|
||||||
|
TotalPrice *float64 `json:"total_price,omitempty"`
|
||||||
|
PatternName string `json:"pattern_name"`
|
||||||
|
PieceLength float64 `json:"piece_length"`
|
||||||
|
PieceWidth float64 `json:"piece_width"`
|
||||||
|
}
|
||||||
30
internal/model/response.go
Normal file
30
internal/model/response.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// ── Common API response envelope ─────────────────────────
|
||||||
|
|
||||||
|
// ApiResponse wraps all API responses.
|
||||||
|
type ApiResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Data any `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApiError wraps error responses.
|
||||||
|
type ApiError struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Simple response types ─────────────────────────────────
|
||||||
|
|
||||||
|
type OKResponse struct {
|
||||||
|
OK string `json:"ok"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobResponse struct {
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadResponse struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
}
|
||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"floorvisualizer/internal/logger"
|
"floorvisualizer/internal/logger"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -182,5 +181,3 @@ func base64enc(data []byte) string {
|
|||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure io is used (for any future imports)
|
|
||||||
var _ = io.Discard
|
|
||||||
|
|||||||
67
main.go
67
main.go
@ -2,19 +2,20 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
"encoding/json"
|
|
||||||
"math/rand"
|
|
||||||
|
|
||||||
|
"floorvisualizer/internal/api"
|
||||||
"floorvisualizer/internal/handler"
|
"floorvisualizer/internal/handler"
|
||||||
"floorvisualizer/internal/logger"
|
"floorvisualizer/internal/logger"
|
||||||
"floorvisualizer/internal/model"
|
|
||||||
"floorvisualizer/internal/middleware"
|
"floorvisualizer/internal/middleware"
|
||||||
|
"floorvisualizer/internal/model"
|
||||||
"floorvisualizer/internal/openrouter"
|
"floorvisualizer/internal/openrouter"
|
||||||
"floorvisualizer/internal/queue"
|
"floorvisualizer/internal/queue"
|
||||||
"floorvisualizer/internal/repository"
|
"floorvisualizer/internal/repository"
|
||||||
@ -23,6 +24,7 @@ import (
|
|||||||
|
|
||||||
const defaultAPIKey = "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"
|
const defaultAPIKey = "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"
|
||||||
const defaultJWTSecret = "floorviz-dev-secret-change-in-production"
|
const defaultJWTSecret = "floorviz-dev-secret-change-in-production"
|
||||||
|
|
||||||
var proxyURL = "http://127.0.0.1:7897"
|
var proxyURL = "http://127.0.0.1:7897"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -132,39 +134,38 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Hot product cache ────────────────────────────────
|
||||||
// ── Hot product cache ────────────────────────────────
|
handler.SetProductsCache(
|
||||||
handler.SetProductsCache(
|
// Getter: check Redis cache for hot products
|
||||||
// Getter: check Redis cache for hot products
|
func(ctx context.Context, sku string) (*model.Product, bool) {
|
||||||
func(ctx context.Context, sku string) (*model.Product, bool) {
|
val, err := rdb.Get(ctx, "product:"+sku).Result()
|
||||||
val, err := rdb.Get(ctx, "product:"+sku).Result()
|
if err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
var p model.Product
|
|
||||||
if json.Unmarshal([]byte(val), &p) == nil {
|
|
||||||
return &p, true
|
|
||||||
}
|
|
||||||
return nil, false
|
return nil, false
|
||||||
},
|
}
|
||||||
// Setter: only cache if this SKU is in the hot list (top 200)
|
var p model.Product
|
||||||
func(ctx context.Context, sku string, product *model.Product) error {
|
if json.Unmarshal([]byte(val), &p) == nil {
|
||||||
rank, err := rdb.ZRevRank(ctx, "product:views", sku).Result()
|
return &p, true
|
||||||
if err != nil || rank >= 200 {
|
}
|
||||||
return nil // not hot enough, skip caching
|
return nil, false
|
||||||
}
|
},
|
||||||
data, _ := json.Marshal(product)
|
// Setter: only cache if this SKU is in the hot list (top 200)
|
||||||
ttl := 1*time.Hour + time.Duration(rand.Intn(600))*time.Second // ±10min jitter
|
func(ctx context.Context, sku string, product *model.Product) error {
|
||||||
return rdb.Set(ctx, "product:"+sku, data, ttl).Err()
|
rank, err := rdb.ZRevRank(ctx, "product:views", sku).Result()
|
||||||
},
|
if err != nil || rank >= 200 {
|
||||||
// View tracker: increment view count in sorted set
|
return nil // not hot enough, skip caching
|
||||||
func(ctx context.Context, sku string) {
|
}
|
||||||
rdb.ZIncrBy(ctx, "product:views", 1, sku)
|
data, _ := json.Marshal(product)
|
||||||
},
|
ttl := 1*time.Hour + time.Duration(rand.Intn(600))*time.Second // ±10min jitter
|
||||||
)
|
return rdb.Set(ctx, "product:"+sku, data, ttl).Err()
|
||||||
|
},
|
||||||
|
// View tracker: increment view count in sorted set
|
||||||
|
func(ctx context.Context, sku string) {
|
||||||
|
rdb.ZIncrBy(ctx, "product:views", 1, sku)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
handler.RegisterAll(mux, client, db, authSvc)
|
api.RegisterRoutes(mux, client, db, authSvc)
|
||||||
|
|
||||||
port := "8099"
|
port := "8099"
|
||||||
if p := os.Getenv("PORT"); p != "" {
|
if p := os.Getenv("PORT"); p != "" {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user