✨ 新增几大中心功能
This commit is contained in:
40
server/service/app/activity.go
Normal file
40
server/service/app/activity.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.echol.cn/loser/Go-Web-Template/server/global"
|
||||
appModel "git.echol.cn/loser/Go-Web-Template/server/model/app"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type AppActivityService struct{}
|
||||
|
||||
var AppActivityServiceApp = new(AppActivityService)
|
||||
|
||||
func (s *AppActivityService) TouchActive(userID uint, ip string) error {
|
||||
now := time.Now()
|
||||
row := appModel.AppUserActivity{
|
||||
AppUserID: userID,
|
||||
LastActiveAt: &now,
|
||||
LastActiveIP: ip,
|
||||
}
|
||||
return global.GVA_DB.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "app_user_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"last_active_at", "last_active_ip", "updated_at"}),
|
||||
}).Create(&row).Error
|
||||
}
|
||||
|
||||
func (s *AppActivityService) RecordLogin(userID uint, ip string) error {
|
||||
now := time.Now()
|
||||
row := appModel.AppUserActivity{
|
||||
AppUserID: userID,
|
||||
LastLoginAt: &now,
|
||||
LastActiveAt: &now,
|
||||
LastActiveIP: ip,
|
||||
}
|
||||
return global.GVA_DB.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "app_user_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"last_login_at", "last_active_at", "last_active_ip", "updated_at"}),
|
||||
}).Create(&row).Error
|
||||
}
|
||||
31
server/service/app/auth.go
Normal file
31
server/service/app/auth.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.echol.cn/loser/Go-Web-Template/server/global"
|
||||
appModel "git.echol.cn/loser/Go-Web-Template/server/model/app"
|
||||
"git.echol.cn/loser/Go-Web-Template/server/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AppAuthService struct{}
|
||||
|
||||
var AppAuthServiceApp = new(AppAuthService)
|
||||
|
||||
func (s *AppAuthService) Login(username, password string) (*appModel.AppUser, error) {
|
||||
var user appModel.AppUser
|
||||
if err := global.GVA_DB.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if user.Enable != 1 {
|
||||
return nil, errors.New("用户被禁止登录")
|
||||
}
|
||||
if !utils.BcryptCheck(password, user.Password) {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
7
server/service/app/enter.go
Normal file
7
server/service/app/enter.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package app
|
||||
|
||||
type ServiceGroup struct {
|
||||
AppAuthService
|
||||
AppRegisterService
|
||||
AppInviteCodeService
|
||||
}
|
||||
132
server/service/app/invite_code.go
Normal file
132
server/service/app/invite_code.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base32"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.echol.cn/loser/Go-Web-Template/server/global"
|
||||
appModel "git.echol.cn/loser/Go-Web-Template/server/model/app"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type AppInviteCodeService struct{}
|
||||
|
||||
var AppInviteCodeServiceApp = new(AppInviteCodeService)
|
||||
|
||||
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 *AppInviteCodeService) GetCurrentUnused(userID uint) (*appModel.AppInviteCode, error) {
|
||||
var record appModel.AppInviteCode
|
||||
now := time.Now()
|
||||
err := global.GVA_DB.
|
||||
Where("created_by_user_id = ? AND status = ?", userID, appModel.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 *AppInviteCodeService) Generate(userID uint) (*InviteCodeGenerated, error) {
|
||||
expireHours := GetInviteExpireHours()
|
||||
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:]
|
||||
}
|
||||
t := time.Now().Add(time.Duration(expireHours) * time.Hour)
|
||||
expiresAt := &t
|
||||
|
||||
record := appModel.AppInviteCode{
|
||||
CreatedByUserID: userID,
|
||||
CodeHash: sha256Hex(code),
|
||||
CodeLast4: last4,
|
||||
Status: appModel.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
|
||||
}
|
||||
|
||||
func (s *AppInviteCodeService) ConsumeByPlainCode(tx *gorm.DB, code string, inviteeUserID uint) (*appModel.AppInviteCode, error) {
|
||||
code = strings.TrimSpace(code)
|
||||
if code == "" {
|
||||
return nil, errors.New("邀请码不能为空")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var record appModel.AppInviteCode
|
||||
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("code_hash = ? AND status = ?", sha256Hex(code), appModel.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 = appModel.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
|
||||
}
|
||||
|
||||
rel := appModel.AppUserInviteRelation{
|
||||
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
|
||||
}
|
||||
68
server/service/app/register.go
Normal file
68
server/service/app/register.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.echol.cn/loser/Go-Web-Template/server/global"
|
||||
appModel "git.echol.cn/loser/Go-Web-Template/server/model/app"
|
||||
appReq "git.echol.cn/loser/Go-Web-Template/server/model/app/request"
|
||||
"git.echol.cn/loser/Go-Web-Template/server/utils"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AppRegisterService struct{}
|
||||
|
||||
var AppRegisterServiceApp = new(AppRegisterService)
|
||||
|
||||
func (s *AppRegisterService) Register(req appReq.PublicRegister, mode RegisterMode) error {
|
||||
username := strings.TrimSpace(req.Username)
|
||||
password := req.Password
|
||||
welcome := strings.TrimSpace(req.WelcomePhrase)
|
||||
|
||||
if username == "" || password == "" || welcome == "" {
|
||||
return errors.New("参数不完整")
|
||||
}
|
||||
if len(welcome) > 255 {
|
||||
return errors.New("欢迎词过长")
|
||||
}
|
||||
if mode == RegisterModeInvite && strings.TrimSpace(req.InviteCode) == "" {
|
||||
return errors.New("邀请码不能为空")
|
||||
}
|
||||
|
||||
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
||||
var existed appModel.AppUser
|
||||
if err := tx.Where("username = ?", username).First(&existed).Error; err == nil {
|
||||
return errors.New("用户名已注册")
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
user := appModel.AppUser{
|
||||
UUID: uuid.New(),
|
||||
Username: username,
|
||||
Password: utils.BcryptHash(password),
|
||||
Enable: 1,
|
||||
}
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
profile := appModel.AppUserProfile{
|
||||
AppUserID: user.ID,
|
||||
WelcomePhrase: welcome,
|
||||
NickName: username,
|
||||
}
|
||||
if err := tx.Create(&profile).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if mode == RegisterModeInvite {
|
||||
if _, err := AppInviteCodeServiceApp.ConsumeByPlainCode(tx, req.InviteCode, user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
15
server/service/app/register_config.go
Normal file
15
server/service/app/register_config.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package app
|
||||
|
||||
import systemSvc "git.echol.cn/loser/Go-Web-Template/server/service/system"
|
||||
|
||||
type RegisterMode = systemSvc.RegisterMode
|
||||
|
||||
const (
|
||||
RegisterModeClosed = systemSvc.RegisterModeClosed
|
||||
RegisterModeOpen = systemSvc.RegisterModeOpen
|
||||
RegisterModeInvite = systemSvc.RegisterModeInvite
|
||||
)
|
||||
|
||||
func GetRegisterMode() RegisterMode { return systemSvc.GetRegisterMode() }
|
||||
func GetRegisterRequireCaptcha() bool { return systemSvc.GetRegisterRequireCaptcha() }
|
||||
func GetInviteExpireHours() int { return systemSvc.GetInviteExpireHours() }
|
||||
Reference in New Issue
Block a user