package reward import ( "encoding/json" "net/http" "strconv" "strings" "github.com/wangjia/pangolin/server/internal/apierr" "github.com/wangjia/pangolin/server/internal/auth" ) const inviteLinkBase = "https://pangolin.yanmeiai.com/i/" type Handler struct { svc *Service st *Store tgEnabled bool channel string } func NewHandler(s *Service, st *Store, tgEnabled bool, channel string) *Handler { return &Handler{svc: s, st: st, tgEnabled: tgEnabled, channel: channel} } func (h *Handler) GetInvite(w http.ResponseWriter, r *http.Request) { ctx := r.Context() uid, ok := auth.UserIDFromContext(ctx) if !ok { apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized) return } code, err := h.svc.EnsureCode(ctx, uid) if err != nil { apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal) return } invited, converted, earned, err := h.st.Summary(ctx, uid) if err != nil { apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal) return } joined, _ := h.st.TelegramClaimed(ctx, uid) w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(map[string]any{ "invite_code": code, "invite_link": inviteLinkBase + code, "invited": invited, "converted": converted, "earned_days": earned, "telegram": map[string]any{ "enabled": h.tgEnabled, "joined": joined, "channel": h.channel, }, }) } func (h *Handler) TelegramStart(w http.ResponseWriter, r *http.Request) { ctx := r.Context() uid, ok := auth.UserIDFromContext(ctx) if !ok { apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized) return } if !h.tgEnabled { apierr.WriteJSON(w, http.StatusNotFound, apierr.ErrNotFound) return } tok, err := h.svc.IssueTelegramToken(ctx, uid) if err != nil { apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(map[string]any{"deep_link": h.svc.TelegramDeepLink(tok)}) } // TelegramWebhook 处理 Telegram Bot API 回调:校验 secret 头 → 解析 `/start ` // → ConsumeTelegramToken → ClaimTelegram → sendMessage 回执。Telegram 只要求 200 即不重投, // 业务结果通过 sendMessage 异步告知用户。 func (h *Handler) TelegramWebhook(w http.ResponseWriter, r *http.Request) { if !h.tgEnabled || r.Header.Get("X-Telegram-Bot-Api-Secret-Token") != h.svc.WebhookSecret() { w.WriteHeader(http.StatusNotFound) return } var upd struct { Message struct { Text string `json:"text"` From struct { ID int64 `json:"id"` } `json:"from"` } `json:"message"` } _ = json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)).Decode(&upd) w.WriteHeader(http.StatusOK) // Telegram 只要 200;业务异步在下方 text, fromID := upd.Message.Text, upd.Message.From.ID if !strings.HasPrefix(text, "/start ") || fromID == 0 { return } token := strings.TrimSpace(strings.TrimPrefix(text, "/start ")) ctx := r.Context() uid, ok, _ := h.svc.ConsumeTelegramToken(ctx, token) if !ok { h.svc.SendTelegram(ctx, fromID, "链接已失效,请回 App 重新点「验证领取」。") return } granted, err := h.svc.ClaimTelegram(ctx, uid, strconv.FormatInt(fromID, 10)) switch { case err != nil: h.svc.SendTelegram(ctx, fromID, "验证出错,请稍后重试。") case granted: h.svc.SendTelegram(ctx, fromID, "已到账 +3 天 Pro,感谢加入!") default: h.svc.SendTelegram(ctx, fromID, "请先加入频道 "+h.svc.Channel()+" 再点验证;若已加入且领取过则无需重复。") } }