Files
Go-Web-Template/server/service/system/invite_code.go
2026-04-23 15:29:07 +08:00

145 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package system
import (
"crypto/rand"
"crypto/sha256"
"encoding/base32"
"encoding/hex"
"errors"
"strings"
"time"
"git.echol.cn/loser/Go-Web-Template/server/global"
sysModel "git.echol.cn/loser/Go-Web-Template/server/model/system"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type InviteCodeService struct{}
var InviteCodeServiceApp = new(InviteCodeService)
type InviteCodeCurrent struct {
HasUnused bool `json:"hasUnused"`
Record *sysModel.InviteCode `json:"record,omitempty"`
}
type InviteCodeGenerated struct {
Code string `json:"code"`
CodeLast4 string `json:"codeLast4"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
}
func sha256Hex(s string) string {
sum := sha256.Sum256([]byte(s))
return hex.EncodeToString(sum[:])
}
func generateCode() (string, error) {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
return "", err
}
enc := base32.StdEncoding.WithPadding(base32.NoPadding)
return strings.ToLower(enc.EncodeToString(buf)), nil
}
func (s *InviteCodeService) GetCurrentUnused(userID uint) (*sysModel.InviteCode, error) {
var record sysModel.InviteCode
now := time.Now()
err := global.GVA_DB.
Where("created_by_user_id = ? AND status = ?", userID, sysModel.InviteCodeStatusUnused).
Where(global.GVA_DB.Where("expires_at IS NULL").Or("expires_at > ?", now)).
Order("id desc").
First(&record).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return &record, err
}
func (s *InviteCodeService) Generate(userID uint, expireHours int) (*InviteCodeGenerated, error) {
if expireHours <= 0 {
expireHours = 24
}
// 单活跃未使用码:存在则拒绝生成(用户已选择该策略)
existing, err := s.GetCurrentUnused(userID)
if err != nil {
return nil, err
}
if existing != nil {
return nil, errors.New("已有未使用邀请码")
}
code, err := generateCode()
if err != nil {
return nil, err
}
last4 := ""
if len(code) >= 4 {
last4 = code[len(code)-4:]
}
var expiresAt *time.Time
if expireHours > 0 {
t := time.Now().Add(time.Duration(expireHours) * time.Hour)
expiresAt = &t
}
record := sysModel.InviteCode{
CreatedByUserID: userID,
CodeHash: sha256Hex(code),
CodeLast4: last4,
Status: sysModel.InviteCodeStatusUnused,
ExpiresAt: expiresAt,
}
if err := global.GVA_DB.Create(&record).Error; err != nil {
return nil, err
}
return &InviteCodeGenerated{Code: code, CodeLast4: last4, ExpiresAt: expiresAt}, nil
}
// ConsumeByPlainCode 校验并消耗邀请码(用于注册邀请码模式,需事务调用)
func (s *InviteCodeService) ConsumeByPlainCode(tx *gorm.DB, code string, inviteeUserID uint) (*sysModel.InviteCode, error) {
code = strings.TrimSpace(code)
if code == "" {
return nil, errors.New("邀请码不能为空")
}
now := time.Now()
var record sysModel.InviteCode
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("code_hash = ? AND status = ?", sha256Hex(code), sysModel.InviteCodeStatusUnused).
Where(tx.Where("expires_at IS NULL").Or("expires_at > ?", now)).
First(&record).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("邀请码无效")
}
return nil, err
}
usedAt := time.Now()
record.Status = sysModel.InviteCodeStatusUsed
record.UsedByUserID = &inviteeUserID
record.UsedAt = &usedAt
if err := tx.Model(&record).Select("status", "used_by_user_id", "used_at").Updates(&record).Error; err != nil {
return nil, err
}
// 写邀请关系invitee 唯一)
rel := sysModel.UserInviteRelation{
InviterUserID: record.CreatedByUserID,
InviteeUserID: inviteeUserID,
InviteCodeID: record.ID,
CreatedAt: time.Now(),
}
if err := tx.Create(&rel).Error; err != nil {
return nil, err
}
return &record, nil
}