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"
|
||||
)
|
||||
|
||||
func handleArticles(db *sql.DB) http.HandlerFunc {
|
||||
func Articles(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// /articles/{id} — detail
|
||||
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) {
|
||||
count := 6
|
||||
if c, err := strconv.Atoi(r.URL.Query().Get("count")); err == nil && c > 0 && c <= 20 {
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"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) {
|
||||
if r.Method != http.MethodPost {
|
||||
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) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeErr(w, 405, "Method not allowed")
|
||||
|
||||
@ -4,68 +4,10 @@ import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"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 {
|
||||
Name string
|
||||
Waste float64
|
||||
@ -90,13 +32,13 @@ var tilePatternsCalc = []struct {
|
||||
|
||||
// ── Handler ─────────────────────────────────────────────────
|
||||
|
||||
func handleCalc(w http.ResponseWriter, r *http.Request) {
|
||||
func Calc(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeErr(w, 405, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
var input CalcInput
|
||||
var input model.CalcInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeJSON(w, 400, map[string]string{"error": "invalid JSON: " + err.Error()})
|
||||
return
|
||||
@ -117,7 +59,7 @@ func handleCalc(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// ── Calculation ─────────────────────────────────────────────
|
||||
|
||||
func calculate(input CalcInput) CalcResult {
|
||||
func calculate(input model.CalcInput) model.CalcResult {
|
||||
patterns := woodPatternsCalc
|
||||
if input.FloorType == "tile" {
|
||||
patterns = tilePatternsCalc
|
||||
@ -127,14 +69,14 @@ func calculate(input CalcInput) CalcResult {
|
||||
}
|
||||
wasteRate := patterns[input.PatternIndex].Waste
|
||||
|
||||
var roomResults []CalcRoomResult
|
||||
var roomResults []model.CalcRoomResult
|
||||
totalRoomArea := 0.0
|
||||
totalDeduct := 0.0
|
||||
|
||||
for _, room := range input.Rooms {
|
||||
area := room.Length * room.Width
|
||||
deductTotal := 0.0
|
||||
var dedResults []CalcDeduction
|
||||
var dedResults []model.CalcDeduction
|
||||
|
||||
for _, d := range room.Deductions {
|
||||
dedArea := deductionArea(d)
|
||||
@ -144,9 +86,11 @@ func calculate(input CalcInput) CalcResult {
|
||||
}
|
||||
|
||||
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,
|
||||
Area: round2(area), Deductions: dedResults,
|
||||
DeductionTotal: round2(deductTotal), NetArea: round2(netArea),
|
||||
@ -156,7 +100,9 @@ func calculate(input CalcInput) CalcResult {
|
||||
}
|
||||
|
||||
totalNetArea := totalRoomArea - totalDeduct
|
||||
if totalNetArea < 0 { totalNetArea = 0 }
|
||||
if totalNetArea < 0 {
|
||||
totalNetArea = 0
|
||||
}
|
||||
|
||||
// Adjust piece for joint
|
||||
effLength := input.PieceLength
|
||||
@ -167,14 +113,16 @@ func calculate(input CalcInput) CalcResult {
|
||||
}
|
||||
|
||||
pieceArea := effLength * effWidth
|
||||
if pieceArea <= 0 { pieceArea = 1 }
|
||||
if pieceArea <= 0 {
|
||||
pieceArea = 1
|
||||
}
|
||||
|
||||
totalPieces := totalNetArea / pieceArea
|
||||
wasteArea := totalNetArea * wasteRate
|
||||
totalArea := totalNetArea + wasteArea
|
||||
piecesNeeded := int(math.Ceil(totalArea / pieceArea))
|
||||
|
||||
result := CalcResult{
|
||||
result := model.CalcResult{
|
||||
FloorType: input.FloorType,
|
||||
Rooms: roomResults,
|
||||
TotalRoomArea: round2(totalRoomArea),
|
||||
@ -214,7 +162,7 @@ func calculate(input CalcInput) CalcResult {
|
||||
return result
|
||||
}
|
||||
|
||||
func deductionArea(d CalcDeduction) float64 {
|
||||
func deductionArea(d model.CalcDeduction) float64 {
|
||||
switch d.Shape {
|
||||
case "circle":
|
||||
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 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 {
|
||||
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 BuildFloorPromptPublic(f FloorOption, p PatternOption, room *RoomOption) string {
|
||||
return buildFloorPrompt(f, p, room)
|
||||
}
|
||||
|
||||
// ── Floor Generate Handler (Redis queue) ──────────────────
|
||||
|
||||
@ -162,7 +168,7 @@ type JobStatusResp struct {
|
||||
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) {
|
||||
if r.Method != http.MethodPost {
|
||||
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})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Progress ───────────────────────────────────────────────
|
||||
|
||||
// ── 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")
|
||||
if jobID == "" {
|
||||
writeErr(w, 400, "job_id is required")
|
||||
@ -244,9 +251,10 @@ func handleFloorStatus(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
writeJSON(w, 200, js)
|
||||
}
|
||||
|
||||
// ── Options ────────────────────────────────────────────────
|
||||
|
||||
func handleFloorOptions(db *sql.DB) http.HandlerFunc {
|
||||
func FloorOptions(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
woodFloors, _ := loadFloorOptions(db, "Hardwood")
|
||||
woodFloors2, _ := loadFloorOptions(db, "Engineered Wood")
|
||||
@ -308,7 +316,7 @@ Style: photorealistic, soft natural matte finish, minimal reflections, no mirror
|
||||
|
||||
// ── Content Check ─────────────────────────────────────────
|
||||
|
||||
func handleCheckContent(client *openrouter.Client) http.HandlerFunc {
|
||||
func CheckContent(client *openrouter.Client) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeErr(w, 405, "Method not allowed")
|
||||
|
||||
@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func handleHealthz(w http.ResponseWriter, r *http.Request) {
|
||||
func Healthz(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, 200, map[string]string{
|
||||
"status": "ok",
|
||||
})
|
||||
|
||||
@ -53,7 +53,7 @@ func SetProductsCache(
|
||||
productsTrackView = tracker
|
||||
}
|
||||
|
||||
func handleProducts(db *sql.DB) http.HandlerFunc {
|
||||
func Products(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Detail: /products/{sku}
|
||||
sku := strings.TrimPrefix(r.URL.Path, "/products/")
|
||||
@ -69,8 +69,7 @@ func handleProducts(db *sql.DB) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
if product == nil {
|
||||
var err error
|
||||
product, err = repository.GetProductBySKU(db, sku)
|
||||
product, err := repository.GetProductBySKU(db, sku)
|
||||
if err != nil {
|
||||
writeJSON(w, 404, map[string]string{"error": "product not found"})
|
||||
return
|
||||
@ -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) {
|
||||
opts, err := repository.GetFilterOptions(db)
|
||||
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) {
|
||||
filters, err := repository.GetFilterHierarchy(db)
|
||||
if err != nil {
|
||||
|
||||
@ -3,7 +3,7 @@ package handler
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"floorvisualizer/internal/logger"
|
||||
"net/http"
|
||||
|
||||
"floorvisualizer/internal/model"
|
||||
@ -21,17 +21,19 @@ type RecommendResponse struct {
|
||||
var productService *service.ProductService
|
||||
|
||||
func initProductService(db *sql.DB) {
|
||||
if productService != nil { return }
|
||||
products, err := repository.QueryAllProducts(db)
|
||||
if err != nil {
|
||||
log.Printf("Failed to load products: %v", err)
|
||||
if productService != nil {
|
||||
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())
|
||||
}
|
||||
|
||||
func handleRecommend(db *sql.DB) http.HandlerFunc {
|
||||
func Recommend(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
sku := r.URL.Query().Get("sku")
|
||||
if sku == "" {
|
||||
|
||||
@ -1,15 +1,11 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"floorvisualizer/internal/middleware"
|
||||
"floorvisualizer/internal/openrouter"
|
||||
"floorvisualizer/internal/service"
|
||||
"floorvisualizer/internal/model"
|
||||
)
|
||||
|
||||
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)}}
|
||||
}
|
||||
|
||||
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) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if status >= 400 {
|
||||
msg := "Unknown error"
|
||||
switch x := v.(type) {
|
||||
@ -80,19 +28,15 @@ func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
if e, ok := x["error"]; ok {
|
||||
msg = e
|
||||
}
|
||||
case map[string]any:
|
||||
if e, ok := x["error"]; ok {
|
||||
msg = fmt.Sprint(e)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(map[string]any{"code": status, "error": msg})
|
||||
json.NewEncoder(w).Encode(model.ApiError{Code: status, Error: msg})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(map[string]any{"code": status, "data": v})
|
||||
json.NewEncoder(w).Encode(model.ApiResponse{Code: status, Data: v})
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
func Upload(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeErr(w, 405, "Method not allowed")
|
||||
return
|
||||
|
||||
@ -18,7 +18,7 @@ import (
|
||||
"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) {
|
||||
userID := middleware.UserID(r.Context())
|
||||
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) {
|
||||
userID := middleware.UserID(r.Context())
|
||||
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) {
|
||||
userID := middleware.UserID(r.Context())
|
||||
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)
|
||||
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 {
|
||||
writeJSON(w, 400, map[string]string{"error": "invalid request body"})
|
||||
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) {
|
||||
userID := middleware.UserID(r.Context())
|
||||
if err := r.ParseMultipartForm(5 << 20); err != nil {
|
||||
@ -233,10 +235,9 @@ func handleUploadAvatar(db *sql.DB) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── 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) {
|
||||
if r.Method != http.MethodPost {
|
||||
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) {
|
||||
userID := middleware.UserID(r.Context())
|
||||
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"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"floorvisualizer/internal/logger"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -182,5 +181,3 @@ func base64enc(data []byte) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Make sure io is used (for any future imports)
|
||||
var _ = io.Discard
|
||||
|
||||
11
main.go
11
main.go
@ -2,19 +2,20 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"time"
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
|
||||
"floorvisualizer/internal/api"
|
||||
"floorvisualizer/internal/handler"
|
||||
"floorvisualizer/internal/logger"
|
||||
"floorvisualizer/internal/model"
|
||||
"floorvisualizer/internal/middleware"
|
||||
"floorvisualizer/internal/model"
|
||||
"floorvisualizer/internal/openrouter"
|
||||
"floorvisualizer/internal/queue"
|
||||
"floorvisualizer/internal/repository"
|
||||
@ -23,6 +24,7 @@ import (
|
||||
|
||||
const defaultAPIKey = "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"
|
||||
const defaultJWTSecret = "floorviz-dev-secret-change-in-production"
|
||||
|
||||
var proxyURL = "http://127.0.0.1:7897"
|
||||
|
||||
func init() {
|
||||
@ -132,7 +134,6 @@ func main() {
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
// ── Hot product cache ────────────────────────────────
|
||||
handler.SetProductsCache(
|
||||
// Getter: check Redis cache for hot products
|
||||
@ -164,7 +165,7 @@ func main() {
|
||||
)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
handler.RegisterAll(mux, client, db, authSvc)
|
||||
api.RegisterRoutes(mux, client, db, authSvc)
|
||||
|
||||
port := "8099"
|
||||
if p := os.Getenv("PORT"); p != "" {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user