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) }