feat: add smoke share token and read-only share APIs
This commit is contained in:
+3
-1
@@ -73,6 +73,7 @@ func main() {
|
|||||||
&smokemodel.SmokeAIAdviceUnlock{},
|
&smokemodel.SmokeAIAdviceUnlock{},
|
||||||
&smokemodel.SmokeAINextSmoke{},
|
&smokemodel.SmokeAINextSmoke{},
|
||||||
&smokemodel.SmokeMotivationQuote{},
|
&smokemodel.SmokeMotivationQuote{},
|
||||||
|
&smokemodel.SmokeShare{},
|
||||||
&marketingmodel.MarketingCategory{},
|
&marketingmodel.MarketingCategory{},
|
||||||
&marketingmodel.MarketingTemplate{},
|
&marketingmodel.MarketingTemplate{},
|
||||||
&marketingmodel.MarketingDownload{},
|
&marketingmodel.MarketingDownload{},
|
||||||
@@ -103,7 +104,8 @@ func main() {
|
|||||||
smokeProfileService := smokeservice.NewSmokeProfileService(database.DB)
|
smokeProfileService := smokeservice.NewSmokeProfileService(database.DB)
|
||||||
smokeNextService := smokeservice.NewSmokeNextService(database.DB)
|
smokeNextService := smokeservice.NewSmokeNextService(database.DB)
|
||||||
smokeAINextService := smokeservice.NewSmokeAINextSmokeService(database.DB, config.AppConfig.AI)
|
smokeAINextService := smokeservice.NewSmokeAINextSmokeService(database.DB, config.AppConfig.AI)
|
||||||
smokeHandler := smokehandler.NewSmokeHandler(smokeLogService, smokeAIAdviceService, smokeProfileService, smokeNextService, smokeAINextService)
|
smokeShareService := smokeservice.NewSmokeShareService(database.DB)
|
||||||
|
smokeHandler := smokehandler.NewSmokeHandler(smokeLogService, smokeAIAdviceService, smokeProfileService, smokeNextService, smokeAINextService, smokeShareService)
|
||||||
|
|
||||||
redeemCodeService := membershipservice.NewRedeemCodeService(database.DB, config.AppConfig.Admin.Token)
|
redeemCodeService := membershipservice.NewRedeemCodeService(database.DB, config.AppConfig.Admin.Token)
|
||||||
redeemCodeHandler := membershiphandler.NewRedeemCodeHandler(redeemCodeService)
|
redeemCodeHandler := membershiphandler.NewRedeemCodeHandler(redeemCodeService)
|
||||||
|
|||||||
@@ -36,5 +36,10 @@ func registerSmokeRoutes(protected *gin.RouterGroup, smokeHandler *smokehandler.
|
|||||||
|
|
||||||
// AI 下次抽烟时间建议(结构化时间节点)
|
// AI 下次抽烟时间建议(结构化时间节点)
|
||||||
smoke.GET("/ai/next_smoke_time", smokeHandler.GetAINextSmokeTime)
|
smoke.GET("/ai/next_smoke_time", smokeHandler.GetAINextSmokeTime)
|
||||||
|
|
||||||
|
// 分享:用于给新用户只读展示统计与历史记录
|
||||||
|
smoke.POST("/share", smokeHandler.CreateShare)
|
||||||
|
smoke.GET("/share/:token", smokeHandler.GetShareView)
|
||||||
|
smoke.POST("/share/:token/revoke", smokeHandler.RevokeShare)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type SmokeHandler struct {
|
|||||||
smokeProfileService *smokeservice.SmokeProfileService
|
smokeProfileService *smokeservice.SmokeProfileService
|
||||||
smokeNextService *smokeservice.SmokeNextService
|
smokeNextService *smokeservice.SmokeNextService
|
||||||
smokeAINextService *smokeservice.SmokeAINextSmokeService
|
smokeAINextService *smokeservice.SmokeAINextSmokeService
|
||||||
|
smokeShareService *smokeservice.SmokeShareService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSmokeHandler(
|
func NewSmokeHandler(
|
||||||
@@ -29,6 +30,7 @@ func NewSmokeHandler(
|
|||||||
smokeProfileService *smokeservice.SmokeProfileService,
|
smokeProfileService *smokeservice.SmokeProfileService,
|
||||||
smokeNextService *smokeservice.SmokeNextService,
|
smokeNextService *smokeservice.SmokeNextService,
|
||||||
smokeAINextService *smokeservice.SmokeAINextSmokeService,
|
smokeAINextService *smokeservice.SmokeAINextSmokeService,
|
||||||
|
smokeShareService *smokeservice.SmokeShareService,
|
||||||
) *SmokeHandler {
|
) *SmokeHandler {
|
||||||
return &SmokeHandler{
|
return &SmokeHandler{
|
||||||
smokeLogService: smokeLogService,
|
smokeLogService: smokeLogService,
|
||||||
@@ -36,6 +38,7 @@ func NewSmokeHandler(
|
|||||||
smokeProfileService: smokeProfileService,
|
smokeProfileService: smokeProfileService,
|
||||||
smokeNextService: smokeNextService,
|
smokeNextService: smokeNextService,
|
||||||
smokeAINextService: smokeAINextService,
|
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])
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SmokeShare 存储用户主动创建的分享令牌,用于只读查看统计与记录。
|
||||||
|
type SmokeShare struct {
|
||||||
|
ID uint `gorm:"primaryKey;comment:主键" json:"id"`
|
||||||
|
CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"-"`
|
||||||
|
|
||||||
|
UID int `gorm:"index;comment:分享发起用户ID" json:"-"`
|
||||||
|
|
||||||
|
ShareToken string `gorm:"column:share_token;size:64;uniqueIndex;comment:分享令牌" json:"share_token"`
|
||||||
|
ExpireAt time.Time `gorm:"column:expire_at;index;comment:过期时间" json:"expire_at"`
|
||||||
|
RevokedAt *time.Time `gorm:"column:revoked_at;comment:撤销时间" json:"revoked_at,omitempty"`
|
||||||
|
|
||||||
|
LastViewedAt *time.Time `gorm:"column:last_viewed_at;comment:最近查看时间" json:"last_viewed_at,omitempty"`
|
||||||
|
ViewCount int64 `gorm:"column:view_count;default:0;comment:查看次数" json:"view_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SmokeShare) TableName() string {
|
||||||
|
return "fa_smoke_share"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SmokeShare) TableComment() string {
|
||||||
|
return "戒烟分享链接"
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"wx_service/internal/model"
|
||||||
|
smokemodel "wx_service/internal/smoke/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrSmokeShareNotFound = errors.New("smoke share not found")
|
||||||
|
ErrSmokeShareExpired = errors.New("smoke share expired")
|
||||||
|
ErrSmokeShareRevoked = errors.New("smoke share revoked")
|
||||||
|
ErrSmokeShareForbidden = errors.New("smoke share forbidden")
|
||||||
|
)
|
||||||
|
|
||||||
|
type SmokeShareService struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateSmokeShareRequest struct {
|
||||||
|
Days int
|
||||||
|
}
|
||||||
|
|
||||||
|
type SmokeShareOwner struct {
|
||||||
|
NickName string `json:"nickname"`
|
||||||
|
AvatarURL string `json:"avatar_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSmokeShareService(db *gorm.DB) *SmokeShareService {
|
||||||
|
return &SmokeShareService{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SmokeShareService) Create(ctx context.Context, uid int, req CreateSmokeShareRequest) (*smokemodel.SmokeShare, error) {
|
||||||
|
days := normalizeShareDays(req.Days)
|
||||||
|
now := time.Now().In(time.Local)
|
||||||
|
|
||||||
|
row := &smokemodel.SmokeShare{
|
||||||
|
UID: uid,
|
||||||
|
ShareToken: generateShareToken(),
|
||||||
|
ExpireAt: now.AddDate(0, 0, days),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.WithContext(ctx).Create(row).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("create smoke share: %w", err)
|
||||||
|
}
|
||||||
|
return row, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SmokeShareService) GetByToken(ctx context.Context, token string) (*smokemodel.SmokeShare, error) {
|
||||||
|
token = strings.TrimSpace(token)
|
||||||
|
if token == "" {
|
||||||
|
return nil, ErrSmokeShareNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
var row smokemodel.SmokeShare
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Where("share_token = ?", token).
|
||||||
|
First(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, ErrSmokeShareNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("load smoke share by token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if row.RevokedAt != nil {
|
||||||
|
return nil, ErrSmokeShareRevoked
|
||||||
|
}
|
||||||
|
if row.ExpireAt.Before(time.Now().In(time.Local)) {
|
||||||
|
return nil, ErrSmokeShareExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
return &row, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SmokeShareService) Revoke(ctx context.Context, uid int, token string) error {
|
||||||
|
token = strings.TrimSpace(token)
|
||||||
|
if token == "" {
|
||||||
|
return ErrSmokeShareNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
var row smokemodel.SmokeShare
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Where("share_token = ?", token).
|
||||||
|
First(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return ErrSmokeShareNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("load smoke share for revoke: %w", err)
|
||||||
|
}
|
||||||
|
if row.UID != uid {
|
||||||
|
return ErrSmokeShareForbidden
|
||||||
|
}
|
||||||
|
if row.RevokedAt != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().In(time.Local)
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Model(&smokemodel.SmokeShare{}).
|
||||||
|
Where("id = ?", row.ID).
|
||||||
|
Update("revoked_at", now).Error; err != nil {
|
||||||
|
return fmt.Errorf("revoke smoke share: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SmokeShareService) TouchViewed(ctx context.Context, id uint) error {
|
||||||
|
now := time.Now().In(time.Local)
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Model(&smokemodel.SmokeShare{}).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"last_viewed_at": now,
|
||||||
|
"view_count": gorm.Expr("view_count + 1"),
|
||||||
|
}).Error; err != nil {
|
||||||
|
return fmt.Errorf("touch smoke share viewed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SmokeShareService) GetOwnerPublic(ctx context.Context, uid int) (*SmokeShareOwner, error) {
|
||||||
|
var user model.User
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Select("id, nick_name, avatar_url").
|
||||||
|
Where("id = ?", uid).
|
||||||
|
First(&user).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return &SmokeShareOwner{NickName: "戒烟用户"}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("load share owner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimSpace(user.NickName)
|
||||||
|
if name == "" {
|
||||||
|
name = "戒烟用户"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SmokeShareOwner{
|
||||||
|
NickName: name,
|
||||||
|
AvatarURL: user.AvatarURL,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeShareDays(days int) int {
|
||||||
|
if days <= 0 {
|
||||||
|
return 7
|
||||||
|
}
|
||||||
|
if days > 30 {
|
||||||
|
return 30
|
||||||
|
}
|
||||||
|
return days
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateShareToken() string {
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
if _, err := rand.Read(buf); err != nil {
|
||||||
|
fallback := time.Now().UnixNano()
|
||||||
|
return fmt.Sprintf("%x", fallback)
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(buf)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user