Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: invitation code #19

Merged
merged 10 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ gen: ## generate CURD code

.PHONY: gen_error_code
gen_error_code: ## generate error code
${GO} generate github.com/TensoRaws/NuxBT-Backend/module/error/gen
${GO} generate github.com/TensoRaws/NuxBT-Backend/module/code/gen

.PHONY: test
test: tidy ## go test
Expand Down
9 changes: 7 additions & 2 deletions conf/nuxbt.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
server:
port: 8080
mode: prod
allowRegister: true
useInvitationCode: false
requestLimit: 50 # 50 times per minute
cros:
- https://114514.com

register:
allowRegister: true
useInvitationCode: true
invitationCodeEligibilityTime: 30 # day, only that users who have registered over xx days can gen invitation code
invitationCodeExpirationTime: 7 # day, invitation code expiration time
invitationCodeLimit: 5 # invitation code limit, one user can gen xx invitation code

jwt:
timeout: 600 # minute
key: nuxbt
Expand Down
153 changes: 153 additions & 0 deletions internal/common/cache/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package cache

import (
"fmt"
"time"

"github.com/TensoRaws/NuxBT-Backend/module/cache"
"github.com/TensoRaws/NuxBT-Backend/module/config"
"github.com/TensoRaws/NuxBT-Backend/module/util"
)

type UserInvitationMapValue struct {
CreatedAt int64 `json:"created_at"`
UsedBy int32 `json:"used_by"`
ExpiresAt int64 `json:"expires_at"`
}

// GenerateInvitationCode 生成邀请码
func GenerateInvitationCode(userID int32) (string, error) {
c := cache.Clients[cache.InvitationCode]

expTime := time.Duration(config.RegisterConfig.InvitationCodeExpirationTime) * time.Hour * 24
code := util.GetRandomString(24)
// 将生成的邀请码存储到 Redis
err := c.Set(code, userID, expTime).Err()
if err != nil {
return "", err
}

toMapString := util.StructToString(UserInvitationMapValue{
CreatedAt: time.Now().Unix(), // 存储邀请码的创建时间
UsedBy: 0, // 初始状态为未使用
ExpiresAt: time.Now().Add(expTime).Unix(), // 过期时间
})

// 将邀请码信息存储到用户的哈希表中,方便查询
err = c.HSet(fmt.Sprintf("user:%d:invitations", userID), code, toMapString).Err()
if err != nil {
return "", err
}

// 更新哈希表键的过期时间,为 10 倍的邀请码过期时间,保证一段时间内可以查询到邀请码状态
err = c.Expire(fmt.Sprintf("user:%d:invitations", userID), 10*expTime).Err()
if err != nil {
return "", err
}

return code, nil
}

type UserInvitation struct {
InvitationCode string `json:"invitation_code"`
UserInvitationMapValue
}

// GetInvitationCodeListByUserID 获取用户近期的邀请码信息
func GetInvitationCodeListByUserID(userID int32) ([]UserInvitation, error) {
c := cache.Clients[cache.InvitationCode]

// 从 Redis 中获取用户的邀请码信息
invitations, err := c.HGetAll(fmt.Sprintf("user:%d:invitations", userID)).Result()
if err != nil {
return nil, err
}

var invitationList []UserInvitation
for code, info := range invitations {
var uim UserInvitationMapValue
err := util.StringToStruct(info, &uim)
if err != nil {
return nil, err
}
invitationList = append(invitationList, UserInvitation{
InvitationCode: code,
UserInvitationMapValue: uim,
})
}

return invitationList, nil
}

// GetValidInvitationCodeCountByUserID 获取用户有效的邀请码数量
func GetValidInvitationCodeCountByUserID(userID int32) (int, error) {
c := cache.Clients[cache.InvitationCode]

invitations, err := c.HGetAll(fmt.Sprintf("user:%d:invitations", userID)).Result()
if err != nil {
return 0, err
}

count := 0
for _, info := range invitations {
var uim UserInvitationMapValue
err := util.StringToStruct(info, &uim)
if err != nil {
return 0, err
}

if uim.UsedBy == 0 && uim.ExpiresAt > time.Now().Unix() {
count++
}
}

return count, nil
}

// ConsumeInvitationCode 注册成功后消费邀请码
func ConsumeInvitationCode(code string, userID int32) error {
c := cache.Clients[cache.InvitationCode]

inviterID, err := c.Get(code).Int()
if err != nil {
return err
}

// 从 Redis 中获取邀请码信息,修改邀请码状态
invitation, err := c.HGet(fmt.Sprintf("user:%d:invitations", inviterID), code).Result()
if err != nil {
return err
}
var uim UserInvitationMapValue
err = util.StringToStruct(invitation, &uim)
if err != nil {
return err
}
uim.UsedBy = userID

// 更新邀请码状态
err = c.HSet(fmt.Sprintf("user:%d:invitations", inviterID), code, util.StructToString(uim)).Err()
if err != nil {
return err
}

// 删除邀请码
err = c.Del(code).Err()
if err != nil {
return err
}

return nil
}

// GetInviterIDByInvitationCode 根据邀请码获取邀请者的 userID
func GetInviterIDByInvitationCode(code string) (int32, error) {
c := cache.Clients[cache.InvitationCode]

userID, err := c.Get(code).Int()
if err != nil {
return 0, err
}

return int32(userID), nil
}
6 changes: 3 additions & 3 deletions internal/common/dao/user.go → internal/common/db/user.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package dao
package db

import (
"github.com/TensoRaws/NuxBT-Backend/dal/model"
Expand All @@ -14,8 +14,8 @@ func CreateUser(user *model.User) (err error) {

// UpdateUserDataByUserID 根据 map 更新用户信息,map 中的 key 为字段名
func UpdateUserDataByUserID(userID int32, maps map[string]interface{}) (err error) {
u := query.User
_, err = u.Where(u.UserID.Eq(userID)).Updates(maps)
q := query.User
_, err = q.Where(q.UserID.Eq(userID)).Updates(maps)
if err != nil {
return err
}
Expand Down
9 changes: 9 additions & 0 deletions internal/router/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ func NewAPI() *gin.Engine {
user.POST("profile/update",
jwt.RequireAuth(cache.Clients[cache.JWTBlacklist], false),
user_service.ProfileUpdate)
// 用户邀请码生成
user.POST("invitation/gen",
jwt.RequireAuth(cache.Clients[cache.JWTBlacklist], false),
user_service.InvitationGen)
// 用户邀请码列表
user.GET("invitation/me",
jwt.RequireAuth(cache.Clients[cache.JWTBlacklist], false),
middleware_cache.Response(cache.Clients[cache.RespCache], 5*time.Second),
user_service.InvitationMe)
}
}

Expand Down
61 changes: 61 additions & 0 deletions internal/service/user/invitation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package user

import (
"fmt"

"github.com/TensoRaws/NuxBT-Backend/internal/common/cache"
"github.com/TensoRaws/NuxBT-Backend/module/code"
"github.com/TensoRaws/NuxBT-Backend/module/config"
"github.com/TensoRaws/NuxBT-Backend/module/log"
"github.com/TensoRaws/NuxBT-Backend/module/resp"
"github.com/gin-gonic/gin"
)

type InvitationGenResponse struct {
InvitationCode string `json:"invitation_code"`
}

type InvitationMeResponse []cache.UserInvitation

// InvitationGen 生成邀请码 (POST /invitation/gen)
func InvitationGen(c *gin.Context) {
userID, _ := resp.GetUserIDFromGinContext(c)

count, err := cache.GetValidInvitationCodeCountByUserID(userID)
if err != nil {
return
}
log.Logger.Infof("User %d has %d valid invitation codes!", userID, count)

if count >= config.RegisterConfig.InvitationCodeLimit {
resp.AbortWithMsg(c, code.UserErrorInvitationCodeHasReachedLimit,
fmt.Sprintf("You have generated %d invitation codes!", count))
return
}

codeGen, err := cache.GenerateInvitationCode(userID)
if err != nil {
resp.AbortWithMsg(c, code.UnknownError, err.Error())
return
}

resp.OKWithData(c, InvitationGenResponse{InvitationCode: codeGen})
log.Logger.Infof("User %d generated invitation code_gen %s successfully!", userID, codeGen)
}

// InvitationMe 获取邀请码列表 (GET /invitation/me)
func InvitationMe(c *gin.Context) {
userID, _ := resp.GetUserIDFromGinContext(c)

codeList, err := cache.GetInvitationCodeListByUserID(userID)
if err != nil {
return
}

if len(codeList) == 0 {
resp.OKWithData(c, InvitationMeResponse{})
} else {
resp.OKWithData(c, InvitationMeResponse(codeList))
}
log.Logger.Infof("User %d got invitation code list successfully!", userID)
}
4 changes: 2 additions & 2 deletions internal/service/user/login.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package user

import (
"github.com/TensoRaws/NuxBT-Backend/internal/common/dao"
"github.com/TensoRaws/NuxBT-Backend/internal/common/db"
"github.com/TensoRaws/NuxBT-Backend/internal/middleware/jwt"
"github.com/TensoRaws/NuxBT-Backend/module/code"
"github.com/TensoRaws/NuxBT-Backend/module/resp"
Expand All @@ -28,7 +28,7 @@ func Login(c *gin.Context) {
}

// GORM 查询
user, err := dao.GetUserByEmail(req.Email)
user, err := db.GetUserByEmail(req.Email)
if err != nil {
resp.AbortWithMsg(c, code.DatabaseErrorRecordNotFound, "User not found")
return
Expand Down
10 changes: 5 additions & 5 deletions internal/service/user/profile.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package user

import (
"github.com/TensoRaws/NuxBT-Backend/internal/common/dao"
"github.com/TensoRaws/NuxBT-Backend/internal/common/db"
"github.com/TensoRaws/NuxBT-Backend/module/code"
"github.com/TensoRaws/NuxBT-Backend/module/log"
"github.com/TensoRaws/NuxBT-Backend/module/resp"
Expand Down Expand Up @@ -31,13 +31,13 @@ type ProfileOthersRequest struct {
func ProfileMe(c *gin.Context) {
userID, _ := resp.GetUserIDFromGinContext(c)

user, err := dao.GetUserByID(userID)
user, err := db.GetUserByID(userID)
if err != nil {
resp.AbortWithMsg(c, code.DatabaseErrorRecordNotFound, "User not found")
return
}

roles, err := dao.GetUserRolesByID(userID)
roles, err := db.GetUserRolesByID(userID)
if err != nil {
log.Logger.Info("Failed to get user roles: " + err.Error())
roles = []string{}
Expand Down Expand Up @@ -73,13 +73,13 @@ func ProfileOthers(c *gin.Context) {
userID, _ := resp.GetUserIDFromGinContext(c)

// 获取信息
user, err := dao.GetUserByID(req.UserID)
user, err := db.GetUserByID(req.UserID)
if err != nil {
resp.AbortWithMsg(c, code.DatabaseErrorRecordNotFound, "User not found")
return
}

roles, err := dao.GetUserRolesByID(req.UserID)
roles, err := db.GetUserRolesByID(req.UserID)
if err != nil {
log.Logger.Info("Failed to get user roles: " + err.Error())
roles = []string{}
Expand Down
4 changes: 2 additions & 2 deletions internal/service/user/profile_update.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package user

import (
"github.com/TensoRaws/NuxBT-Backend/internal/common/dao"
"github.com/TensoRaws/NuxBT-Backend/internal/common/db"
"github.com/TensoRaws/NuxBT-Backend/module/code"
"github.com/TensoRaws/NuxBT-Backend/module/log"
"github.com/TensoRaws/NuxBT-Backend/module/resp"
Expand Down Expand Up @@ -61,7 +61,7 @@ func ProfileUpdate(c *gin.Context) {
updates["background"] = *req.Background
}
// 执行更新
err := dao.UpdateUserDataByUserID(userID, updates)
err := db.UpdateUserDataByUserID(userID, updates)
if err != nil {
resp.AbortWithMsg(c, code.DatabaseErrorRecordUpdateFailed, err.Error())
return
Expand Down
Loading
Loading