package handler import ( "errors" "fmt" "io" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "wx_service/internal/middleware" "wx_service/internal/model" smokeservice "wx_service/internal/smoke/service" ) type createSmokeShareRequest struct { Days *int `json:"days"` } func (h *SmokeHandler) CreateShare(c *gin.Context) { user := middleware.MustCurrentUser(c) var req createSmokeShareRequest if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误")) return } days := 0 if req.Days != nil { days = *req.Days } share, err := h.smokeShareService.Create(c.Request.Context(), int(user.ID), smokeservice.CreateSmokeShareRequest{Days: days}) if err != nil { c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "创建分享失败,请稍后重试")) return } c.JSON(http.StatusOK, model.Success(gin.H{ "share_token": share.ShareToken, "expire_at": share.ExpireAt.Format(time.RFC3339), "share_path": fmt.Sprintf("pages/share/index?share_token=%s", share.ShareToken), })) } func (h *SmokeHandler) GetShareView(c *gin.Context) { token := strings.TrimSpace(c.Param("token")) if token == "" { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "share_token 不能为空")) return } share, err := h.smokeShareService.GetByToken(c.Request.Context(), token) if err != nil { switch { case errors.Is(err, smokeservice.ErrSmokeShareNotFound): c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "分享不存在")) case errors.Is(err, smokeservice.ErrSmokeShareExpired): c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "分享已过期")) case errors.Is(err, smokeservice.ErrSmokeShareRevoked): c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "分享已失效")) default: c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "加载分享失败,请稍后重试")) } return } _ = h.smokeShareService.TouchViewed(c.Request.Context(), share.ID) rangeType := strings.ToLower(strings.TrimSpace(c.DefaultQuery("range", "week"))) asOf := time.Now().In(time.Local) if v := strings.TrimSpace(c.Query("date")); v != "" { parsed, parseErr := time.ParseInLocation(dateLayout, v, time.Local) if parseErr != nil { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "date 格式错误,应为 YYYY-MM-DD")) return } asOf = time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 23, 59, 59, 0, time.Local) } statsReq, err := buildStatsRequest(rangeType, asOf) if err != nil { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error())) return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) listType := strings.ToLower(strings.TrimSpace(c.DefaultQuery("type", "all"))) if listType != "all" && listType != "smoke" && listType != "resisted" { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "type 应为 all|smoke|resisted")) return } owner, err := h.smokeShareService.GetOwnerPublic(c.Request.Context(), share.UID) if err != nil { c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "加载分享用户信息失败")) return } profile, err := h.smokeProfileService.Get(c.Request.Context(), share.UID) if err != nil { c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取基础信息失败,请稍后重试")) return } stats, err := h.smokeLogService.Stats(c.Request.Context(), share.UID, statsReq, profile) if err != nil { c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取统计数据失败,请稍后重试")) return } homeSummary, err := h.smokeLogService.HomeSummary(c.Request.Context(), share.UID, asOf) if err != nil { c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取首页汇总失败,请稍后重试")) return } logs, err := h.smokeLogService.List(c.Request.Context(), share.UID, smokeservice.ListSmokeLogsRequest{ Page: page, PageSize: pageSize, Type: listType, }) if err != nil { c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "查询记录失败,请稍后重试")) return } c.JSON(http.StatusOK, model.Success(gin.H{ "owner": gin.H{ "nickname": maskShareNickname(owner.NickName), "avatar_url": owner.AvatarURL, }, "share": gin.H{ "share_token": share.ShareToken, "expire_at": share.ExpireAt, "last_viewed_at": share.LastViewedAt, "view_count": share.ViewCount + 1, }, "overview": gin.H{ "today_count": homeSummary.TodayCount, "resisted_count": homeSummary.ResistedCount, "reduced_from_yesterday": homeSummary.ReducedFromYesterday, "exceeded_yesterday": homeSummary.ExceededYesterday, "last_smoke_at": homeSummary.LastSmokeAt, "seconds_since_last": homeSummary.SecondsSinceLast, "streak_days": stats.StreakDays, }, "stats": stats, "logs": gin.H{ "items": logs.Items, "total": logs.Total, "page": logs.Page, "page_size": logs.PageSize, }, })) } func (h *SmokeHandler) RevokeShare(c *gin.Context) { user := middleware.MustCurrentUser(c) token := strings.TrimSpace(c.Param("token")) if token == "" { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "share_token 不能为空")) return } err := h.smokeShareService.Revoke(c.Request.Context(), int(user.ID), token) if err != nil { switch { case errors.Is(err, smokeservice.ErrSmokeShareNotFound): c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "分享不存在")) case errors.Is(err, smokeservice.ErrSmokeShareForbidden): c.JSON(http.StatusForbidden, model.Error(http.StatusForbidden, "无权限操作该分享")) default: c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "撤销分享失败,请稍后重试")) } return } c.JSON(http.StatusOK, model.Success(gin.H{"revoked": true})) } func maskShareNickname(name string) string { runes := []rune(strings.TrimSpace(name)) if len(runes) <= 1 { return "戒烟用户" } if len(runes) == 2 { return string(runes[0]) + "*" } return string(runes[0]) + strings.Repeat("*", len(runes)-2) + string(runes[len(runes)-1]) }