feat: add smoke share token and read-only share APIs

This commit is contained in:
root
2026-03-10 18:53:01 +08:00
parent a4b466744c
commit 484aa96a6e
6 changed files with 409 additions and 1 deletions
+3
View File
@@ -21,6 +21,7 @@ type SmokeHandler struct {
smokeProfileService *smokeservice.SmokeProfileService
smokeNextService *smokeservice.SmokeNextService
smokeAINextService *smokeservice.SmokeAINextSmokeService
smokeShareService *smokeservice.SmokeShareService
}
func NewSmokeHandler(
@@ -29,6 +30,7 @@ func NewSmokeHandler(
smokeProfileService *smokeservice.SmokeProfileService,
smokeNextService *smokeservice.SmokeNextService,
smokeAINextService *smokeservice.SmokeAINextSmokeService,
smokeShareService *smokeservice.SmokeShareService,
) *SmokeHandler {
return &SmokeHandler{
smokeLogService: smokeLogService,
@@ -36,6 +38,7 @@ func NewSmokeHandler(
smokeProfileService: smokeProfileService,
smokeNextService: smokeNextService,
smokeAINextService: smokeAINextService,
smokeShareService: smokeShareService,
}
}
@@ -0,0 +1,196 @@
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])
}