feat: add smoke share token and read-only share APIs
This commit is contained in:
@@ -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