package codes import ( "encoding/json" "net/http" "github.com/wangjia/pangolin/server/internal/apierr" ) // RedeemHandler handles POST /v1/redeem requests. // It expects the request context to carry the authenticated userID under the // key ctxKeyUserID (set by the JWT auth middleware from the auth module). type RedeemHandler struct { svc *Service } // NewRedeemHandler creates a RedeemHandler backed by svc. func NewRedeemHandler(svc *Service) *RedeemHandler { return &RedeemHandler{svc: svc} } // ctxKeyUserID is the context key for the authenticated user ID. // Defined here to avoid an import cycle; the auth middleware uses the same key. type ctxKey string const CtxKeyUserID ctxKey = "user_id" // redeemRequest is the JSON body for POST /v1/redeem. type redeemRequest struct { Code string `json:"code"` } // redeemResponse is the JSON body on success. type redeemResponse struct { Idempotent bool `json:"idempotent"` Plan string `json:"plan"` DurationDays int `json:"duration_days"` ExpiresAt string `json:"expires_at,omitempty"` // RFC 3339 UTC } // ServeHTTP implements http.Handler. func (h *RedeemHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } // Extract authenticated userID from context (set by JWT middleware). userID, ok := r.Context().Value(CtxKeyUserID).(int64) if !ok || userID == 0 { apierr.WriteJSON(w, http.StatusUnauthorized, &apierr.Error{ Code: "UNAUTHORIZED", MessageZH: "请先登录", MessageEn: "Authentication required", }) return } var req redeemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Code == "" { apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest) return } result, apiErr := h.svc.Redeem(r.Context(), RedeemRequest{ UserID: userID, Code: req.Code, }) if apiErr != nil { status := http.StatusBadRequest switch apiErr.Code { case "ACCOUNT_LOCKED", "RATE_LIMITED": status = http.StatusTooManyRequests case "INTERNAL_ERROR": status = http.StatusInternalServerError case "CODE_REDEEMED", "CODE_NOT_FOUND", "CODE_VOID", "INVALID_CODE": status = http.StatusBadRequest } apierr.WriteJSON(w, status, apiErr) return } resp := redeemResponse{ Idempotent: result.Idempotent, Plan: string(result.PlanCode), DurationDays: result.DurationDays, } if !result.ExpiresAt.IsZero() { resp.ExpiresAt = result.ExpiresAt.UTC().Format("2006-01-02T15:04:05Z") } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(resp) }