package usage import ( "encoding/json" "net/http" "strconv" "github.com/wangjia/pangolin/server/internal/apierr" ) // ctxKey is the context key type for request-scoped values. Defined here to // avoid an import cycle; the JWT auth middleware sets the same key. type ctxKey string // CtxKeyUserID is the context key carrying the authenticated int64 user ID. const CtxKeyUserID ctxKey = "user_id" // userIDFromContext extracts the authenticated user ID set by the auth // middleware, or 0 if absent. func userIDFromContext(r *http.Request) int64 { id, _ := r.Context().Value(CtxKeyUserID).(int64) return id } // UsageHandler serves GET /v1/usage?days=N. type UsageHandler struct { svc *Service } // NewUsageHandler creates a UsageHandler. func NewUsageHandler(svc *Service) *UsageHandler { return &UsageHandler{svc: svc} } type usageResponse struct { Points []UsagePoint `json:"points"` } // ServeHTTP implements http.Handler. func (h *UsageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } userID := userIDFromContext(r) if userID == 0 { apierr.WriteJSON(w, http.StatusUnauthorized, &apierr.Error{ Code: "UNAUTHORIZED", MessageZH: "请先登录", MessageEn: "Authentication required", }) return } days := 7 if v := r.URL.Query().Get("days"); v != "" { n, err := strconv.Atoi(v) if err != nil || n < 1 || n > 90 { apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest) return } days = n } points, apiErr := h.svc.UsageCurve(r.Context(), userID, days) if apiErr != nil { apierr.WriteJSON(w, http.StatusInternalServerError, apiErr) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(usageResponse{Points: points}) } // AdsUnlockHandler serves POST /v1/ads/unlock. type AdsUnlockHandler struct { svc *Service } // NewAdsUnlockHandler creates an AdsUnlockHandler. func NewAdsUnlockHandler(svc *Service) *AdsUnlockHandler { return &AdsUnlockHandler{svc: svc} } type adsUnlockRequest struct { DeviceID string `json:"device_id"` AdToken string `json:"ad_token"` } // ServeHTTP implements http.Handler. On success it returns 204 No Content // (matching the OpenAPI contract), including the idempotent already-unlocked // case. func (h *AdsUnlockHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } userID := userIDFromContext(r) if userID == 0 { apierr.WriteJSON(w, http.StatusUnauthorized, &apierr.Error{ Code: "UNAUTHORIZED", MessageZH: "请先登录", MessageEn: "Authentication required", }) return } var req adsUnlockRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16*1024)).Decode(&req); err != nil { apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest) return } _, apiErr := h.svc.UnlockAd(r.Context(), userID, req.DeviceID, req.AdToken) if apiErr != nil { apierr.WriteJSON(w, adsErrorStatus(apiErr.Code), apiErr) return } w.WriteHeader(http.StatusNoContent) } // adsErrorStatus maps an ads-unlock error code to an HTTP status. func adsErrorStatus(code string) int { switch code { case "AD_VERIFY_FAILED": return http.StatusForbidden case "AD_TOKEN_REPLAY": return http.StatusConflict case "BAD_REQUEST": return http.StatusBadRequest case "INTERNAL_ERROR": return http.StatusInternalServerError default: return http.StatusBadRequest } }