Files
web_hghl_server/app/service/authser/auth.go
T
2026-10-26 16:28:14 +08:00

588 lines
18 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 authser
import (
"context"
"math/rand"
"strconv"
"time"
"videoweb/app/appg"
"videoweb/app/service/configser"
"videoweb/pub/common/enum/consts"
"videoweb/pub/common/log"
"videoweb/pub/common/netutil"
"videoweb/pub/common/srvdef"
"videoweb/pub/common/utils"
"videoweb/pub/models/usermod"
"videoweb/pub/mongos"
"videoweb/pub/pubdef"
"videoweb/pub/redis"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
"golang.org/x/crypto/bcrypt"
)
var tokener = utils.NewTokenRedis()
var defaultAvatars = []string{
"v3/image/bo/ty/1016/girl1.jpeg",
"v3/image/re/9y/1016/girl2.jpeg",
"v3/image/5f/k4/1016/girl3.jpeg",
"v3/image/ej/ss/1016/girl4.jpeg",
"v3/image/yq/ys/1016/girl5.jpeg",
"v3/image/9e/d3/1016/girl6.jpeg",
}
func randomAvatar() string {
return defaultAvatars[time.Now().UnixNano()%int64(len(defaultAvatars))]
}
// generateUniqueInviteCode 生成 8 位唯一邀请码(含大小写字母),最多重试 10 次
func generateUniqueInviteCode(ctx context.Context) string {
for i := 0; i < 10; i++ {
code := utils.GetRandomLetters(8)
exist, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.InviteCode: code})
if exist == nil {
return code
}
log.Warn("authser:generateUniqueInviteCode collision, retrying", log.Any("code", code))
}
// 极小概率走到这里:用更长的随机串兜底
return utils.GetRandomLetters(12)
}
// LoginResp 登录/注册成功后的响应数据
type LoginResp struct {
Token string `json:"token"`
UserId int64 `json:"userId"`
AccountNo string `json:"accountNo"`
NickName string `json:"nickName"`
Status pubdef.UserStatus `json:"status"`
Avatar string `json:"avatar"` // 头像
InviteCode string `json:"inviteCode"` // 邀请码
LoginType pubdef.LoginType `json:"loginType"` // 登录方式 0=设备 3=账号
IsRegister bool `json:"isRegister"` // 是否已完成账号注册
IsNewUser bool `json:"isNewUser"` // 是否本次请求新创建的用户
Recharge int64 `json:"recharge"` // 当前金币个数
}
// UserInfoResp 当前登录用户信息
type UserInfoResp struct {
UserId int64 `json:"userId"`
AccountNo string `json:"accountNo"`
NickName string `json:"nickName"`
Status pubdef.UserStatus `json:"status"`
Avatar string `json:"avatar"`
InviteCode string `json:"inviteCode"`
LoginType pubdef.LoginType `json:"loginType"`
IsRegister bool `json:"isRegister"`
Recharge int64 `json:"recharge"`
}
// UAInfo 从 UA 中间件提取的设备/系统信息
type UAInfo struct {
Device string // 设备型号(DevType,如 "iPhone")
SysType pubdef.SysType // 系统类型(ios / android / h5 等)
AppVer string // App 版本号
IP string // 客户端 IP
}
// GuestLoginReq 游客登录请求
type GuestLoginReq struct {
DevID string // 设备 ID(首选)
Mac string // 设备 MAC 地址(备用)
DeviceId string // 设备 ID 第二版本(备用)
UA UAInfo // 设备/系统信息
}
// hashPassword 使用 bcrypt 对明文密码进行哈希
func hashPassword(plain string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}
// checkPassword 验证明文密码与 bcrypt 哈希是否匹配
func checkPassword(plain, hash string) bool {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain)) == nil
}
// generateAndSaveToken 生成 JWT 并存入 Redis
func generateAndSaveToken(userId int64) (string, *srvdef.SrvErr) {
token, err := utils.GenerateUserToken(utils.UserClaims{
UserID: userId,
Expire: consts.UserJwtTokenExpire,
}, appg.Conf.JwtKey)
if err != nil {
return "", srvdef.New(srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg(), "")
}
if err = tokener.Save(userId, token); err != nil {
return "", srvdef.New(srvdef.StoreLevel, "token存储失败", "")
}
return token, nil
}
// GuestLogin 游客登录:按设备 ID 查找已有用户,找不到则自动注册
// 若请求携带有效 token(currentUserId != 0),优先复用该账号
func GuestLogin(ctx context.Context, req *GuestLoginReq, currentUserId int64) (*LoginResp, *srvdef.SrvErr) {
var user *usermod.User
// 优先用 token 对应的已有用户
if currentUserId > 0 {
u, _ := usermod.FindOneUserById(ctx, currentUserId)
if u != nil {
user = u
}
}
// 按设备 ID 查找
if user == nil {
user, _ = findUserByDevice(ctx, req.DevID, req.Mac, req.DeviceId)
}
isNewUser := false
if user == nil {
// 未找到 → 自动注册游客
var serr *srvdef.SrvErr
user, serr = registerGuest(ctx, req)
if serr != nil {
return nil, serr
}
isNewUser = true
} else {
// 已有用户:更新设备信息和 UA(静默,不影响主流程)
updateSet := bson.M{
usermod.FN.User.LastLoginAt: time.Now(),
usermod.FN.User.UpdatedAt: time.Now(),
}
if req.DevID != "" {
updateSet[usermod.FN.User.DevID] = req.DevID
}
if req.Mac != "" && req.Mac != "00:00:00:00:00:00" {
updateSet[usermod.FN.User.Mac] = req.Mac
}
if req.DeviceId != "" {
updateSet[usermod.FN.User.DevV2] = req.DeviceId
}
if req.UA.Device != "" {
updateSet[usermod.FN.User.Device] = req.UA.Device
}
if req.UA.SysType != "" {
updateSet[usermod.FN.User.SysType] = req.UA.SysType.String()
}
if req.UA.AppVer != "" {
updateSet[usermod.FN.User.AppVer] = req.UA.AppVer
}
if req.UA.IP != "" {
updateSet[usermod.FN.User.LastIp] = req.UA.IP
}
_ = usermod.UpdateOneUser(ctx,
bson.M{usermod.FN.User.Id: user.Id},
bson.M{"$set": updateSet},
)
}
if user.Status == pubdef.UserStatusDisable {
return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
}
token, serr := generateAndSaveToken(user.Id)
if serr != nil {
return nil, serr
}
cdn := configser.ImageCdnFromCtx(ctx)
return &LoginResp{
Token: token,
UserId: user.Id,
AccountNo: user.AccountNo,
NickName: user.NickName,
Status: user.Status,
Avatar: netutil.MergeURL(cdn, user.Avatar),
InviteCode: user.InviteCode,
LoginType: user.LoginType,
IsRegister: user.IsRegister,
IsNewUser: isNewUser,
Recharge: user.Recharge,
}, nil
}
// GetInfo 根据当前登录 token 解析出的用户 ID 查询用户信息
func GetInfo(ctx context.Context, userId int64) (*UserInfoResp, *srvdef.SrvErr) {
if userId <= 0 {
return nil, srvdef.New(srvdef.InvalidToken, srvdef.InvalidToken.Msg(), "")
}
user, serr := usermod.FindOneUserById(ctx, userId, options.FindOne().SetProjection(userInfoProjection()))
if serr != nil {
return nil, serr
}
if user == nil {
return nil, srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "")
}
if user.Status == pubdef.UserStatusDisable {
return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
}
cdn := configser.ImageCdnFromCtx(ctx)
return &UserInfoResp{
UserId: user.Id,
AccountNo: user.AccountNo,
NickName: user.NickName,
Status: user.Status,
Avatar: netutil.MergeURL(cdn, user.Avatar),
InviteCode: user.InviteCode,
LoginType: user.LoginType,
IsRegister: user.IsRegister,
Recharge: user.Recharge,
}, nil
}
func userInfoProjection() bson.M {
return bson.M{
usermod.FN.User.Id: 1,
usermod.FN.User.AccountNo: 1,
usermod.FN.User.NickName: 1,
usermod.FN.User.Status: 1,
usermod.FN.User.Avatar: 1,
usermod.FN.User.InviteCode: 1,
usermod.FN.User.LoginType: 1,
usermod.FN.User.IsRegister: 1,
usermod.FN.User.Recharge: 1,
}
}
// findUserByDevice 按设备 ID 三级回退查找用户(DevID → Mac → DeviceId)
func findUserByDevice(ctx context.Context, devID, mac, deviceId string) (*usermod.User, *srvdef.SrvErr) {
invalidMac := "00:00:00:00:00:00"
if devID != "" {
u, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevID: devID})
if u != nil {
return u, nil
}
}
if mac != "" && mac != invalidMac {
u, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Mac: mac})
if u != nil {
return u, nil
}
}
if deviceId != "" && deviceId != invalidMac {
u, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevV2: deviceId})
if u != nil {
return u, nil
}
}
return nil, nil
}
// registerGuest 自动创建游客用户
func registerGuest(ctx context.Context, req *GuestLoginReq) (*usermod.User, *srvdef.SrvErr) {
now := time.Now()
user := &usermod.User{
Id: mongos.Lid.User.Get(),
NickName: getName(),
Avatar: randomAvatar(),
InviteCode: generateUniqueInviteCode(ctx),
Status: pubdef.UserStatusNormal,
LoginType: pubdef.LoginTypeDevice,
IsRegister: false,
DevID: req.DevID,
Mac: req.Mac,
DevV2: req.DeviceId,
Device: req.UA.Device,
SysType: req.UA.SysType.String(),
AppVer: req.UA.AppVer,
OriVer: req.UA.AppVer,
RegIp: req.UA.IP,
LastIp: req.UA.IP,
RegisterTime: now,
CreatedAt: now,
UpdatedAt: now,
}
if serr := usermod.InsertOneUser(ctx, user); serr != nil {
return nil, serr
}
return user, nil
}
// Register 账号注册
// currentUserId != 0 时为游客升级模式:在已有游客账号上绑定账号密码
// currentUserId == 0 时为直接注册新账号
func Register(ctx context.Context, accountNo, password, agentInviteCode string, currentUserId int64, ua UAInfo) (*LoginResp, *srvdef.SrvErr) {
// 检查 accountNo 是否已被其他用户占用
exist, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: accountNo})
if exist != nil && exist.Id != currentUserId {
return nil, srvdef.New(srvdef.ErrDbInputExist, "账号已存在", "")
}
hashed, err := hashPassword(password)
if err != nil {
return nil, srvdef.New(srvdef.Failure, "密码处理失败", "")
}
now := time.Now()
// 游客升级:更新已有记录
if currentUserId > 0 {
user, _ := usermod.FindOneUserById(ctx, currentUserId)
if user == nil {
return nil, srvdef.New(srvdef.ErrUserNotExsit, "用户不存在", "")
}
if user.IsRegister {
return nil, srvdef.New(srvdef.ErrDbInputExist, "已完成账号注册,无需重复操作", "")
}
setDoc := bson.M{
usermod.FN.User.AccountNo: accountNo,
usermod.FN.User.Password: hashed,
usermod.FN.User.IsRegister: true,
usermod.FN.User.LoginType: pubdef.LoginTypeAccount,
usermod.FN.User.UpdatedAt: now,
}
if agentInviteCode != "" && user.AgentInviteCode == "" {
setDoc[usermod.FN.User.AgentInviteCode] = agentInviteCode
}
if ua.Device != "" {
setDoc[usermod.FN.User.Device] = ua.Device
}
if ua.SysType != "" {
setDoc[usermod.FN.User.SysType] = ua.SysType.String()
}
if ua.AppVer != "" {
setDoc[usermod.FN.User.AppVer] = ua.AppVer
}
if ua.IP != "" {
setDoc[usermod.FN.User.LastIp] = ua.IP
}
if serr := usermod.UpdateOneUser(ctx,
bson.M{usermod.FN.User.Id: currentUserId},
bson.M{"$set": setDoc},
); serr != nil {
return nil, serr
}
// 给邀请人 +1
if agentInviteCode != "" {
_ = usermod.UpdateOneUser(ctx,
bson.M{usermod.FN.User.InviteCode: agentInviteCode},
bson.M{"$inc": bson.M{usermod.FN.User.Invites: 1},
"$set": bson.M{usermod.FN.User.UpdatedAt: time.Now()}},
)
}
token, serr := generateAndSaveToken(user.Id)
if serr != nil {
return nil, serr
}
cdn := configser.ImageCdnFromCtx(ctx)
return &LoginResp{
Token: token,
UserId: user.Id,
AccountNo: accountNo,
NickName: user.NickName,
Status: user.Status,
Avatar: netutil.MergeURL(cdn, user.Avatar),
InviteCode: user.InviteCode,
LoginType: pubdef.LoginTypeAccount,
IsRegister: true,
Recharge: user.Recharge,
}, nil
}
// 未登录:直接注册新账号
user := &usermod.User{
Id: mongos.Lid.User.Get(),
AccountNo: accountNo,
Password: hashed,
NickName: getName(),
Avatar: randomAvatar(),
InviteCode: generateUniqueInviteCode(ctx),
AgentInviteCode: agentInviteCode,
Status: pubdef.UserStatusNormal,
IsRegister: true,
LoginType: pubdef.LoginTypeAccount,
Device: ua.Device,
SysType: ua.SysType.String(),
AppVer: ua.AppVer,
OriVer: ua.AppVer,
RegIp: ua.IP,
LastIp: ua.IP,
RegisterTime: now,
CreatedAt: now,
UpdatedAt: now,
}
if serr := usermod.InsertOneUser(ctx, user); serr != nil {
return nil, serr
}
if agentInviteCode != "" {
_ = usermod.UpdateOneUser(ctx,
bson.M{usermod.FN.User.InviteCode: agentInviteCode},
bson.M{
"$inc": bson.M{usermod.FN.User.Invites: 1},
"$set": bson.M{usermod.FN.User.UpdatedAt: time.Now()},
},
)
}
token, serr := generateAndSaveToken(user.Id)
if serr != nil {
return nil, serr
}
cdn := configser.ImageCdnFromCtx(ctx)
return &LoginResp{
Token: token,
UserId: user.Id,
AccountNo: user.AccountNo,
NickName: user.NickName,
Status: user.Status,
Avatar: netutil.MergeURL(cdn, user.Avatar),
InviteCode: user.InviteCode,
LoginType: pubdef.LoginTypeAccount,
IsRegister: true,
}, nil
}
// Login 用户登录(账号均可)
func Login(ctx context.Context, account, password string, ua UAInfo) (*LoginResp, *srvdef.SrvErr) {
user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: account})
if user == nil {
return nil, srvdef.New(srvdef.AccountIsNotExists, srvdef.AccountIsNotExists.Msg(), "")
}
if user.Status == pubdef.UserStatusDisable {
return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
}
if !checkPassword(password, user.Password) {
return nil, srvdef.New(srvdef.AccountErrPasswordError, srvdef.AccountErrPasswordError.Msg(), "")
}
token, serr := generateAndSaveToken(user.Id)
if serr != nil {
return nil, serr
}
// 更新最后登录时间、IP 及 UA(fire-and-forget)
loginSet := bson.M{
usermod.FN.User.LastLoginAt: time.Now(),
usermod.FN.User.UpdatedAt: time.Now(),
}
if ua.IP != "" {
loginSet[usermod.FN.User.LastIp] = ua.IP
}
if ua.Device != "" {
loginSet[usermod.FN.User.Device] = ua.Device
}
if ua.SysType != "" {
loginSet[usermod.FN.User.SysType] = ua.SysType.String()
}
if ua.AppVer != "" {
loginSet[usermod.FN.User.AppVer] = ua.AppVer
}
_ = usermod.UpdateOneUser(ctx,
bson.M{usermod.FN.User.Id: user.Id},
bson.M{"$set": loginSet},
)
cdn := configser.ImageCdnFromCtx(ctx)
return &LoginResp{
Token: token,
UserId: user.Id,
AccountNo: user.AccountNo,
NickName: user.NickName,
Status: user.Status,
Avatar: netutil.MergeURL(cdn, user.Avatar),
InviteCode: user.InviteCode,
LoginType: user.LoginType,
IsRegister: user.IsRegister,
Recharge: user.Recharge,
}, nil
}
// UpdateProfileReq 修改用户资料的参数(所有字段均为可选)
type UpdateProfileReq struct {
AccountNo string // 用户账号(用于定位用户,必传)
Avatar *string // 新头像 URL(可选)
NikeName *string // 昵称
OldPassword *string // 旧密码明文(修改密码时必传)
NewPassword *string // 新密码明文(修改密码时必传)
}
// UpdateProfile 修改用户资料:支持修改头像、密码,或两者同时修改
func UpdateProfile(ctx context.Context, req *UpdateProfileReq) *srvdef.SrvErr {
user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: req.AccountNo})
if user == nil {
return srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "")
}
setDoc := bson.M{usermod.FN.User.UpdatedAt: time.Now()}
// 修改头像
if req.Avatar != nil && *req.Avatar != "" {
setDoc[usermod.FN.User.Avatar] = *req.Avatar
}
if req.NikeName != nil && *req.NikeName != "" {
setDoc[usermod.FN.User.NickName] = *req.NikeName
}
// 修改密码:两者必须同时提供
if req.OldPassword != nil || req.NewPassword != nil {
if req.OldPassword == nil || req.NewPassword == nil {
return srvdef.New(srvdef.ErrParamError, "修改密码时旧密码和新密码均需提供", "")
}
if !checkPassword(*req.OldPassword, user.Password) {
return srvdef.New(srvdef.AccountErrPasswordError, "旧密码错误", "")
}
hashed, err := hashPassword(*req.NewPassword)
if err != nil {
return srvdef.New(srvdef.Failure, "密码处理失败", "")
}
setDoc[usermod.FN.User.Password] = hashed
}
// 没有任何需要更新的字段
if len(setDoc) == 1 {
return srvdef.New(srvdef.ErrParamError, "至少需要修改一项资料", "")
}
return usermod.UpdateOneUser(ctx,
bson.M{usermod.FN.User.Id: user.Id},
bson.M{"$set": setDoc},
)
}
func getName() string {
const limit = 300
totalCnt, err := redis.SCard(redis.NameSource)
if err != nil || totalCnt < limit {
if len(appg.Name.FamilyNames) == 0 || len(appg.Name.AfterNames) == 0 {
// fmt.Println(totalCnt, err, appg.Name.FamilyNames)
return "游客" + strconv.FormatInt(time.Now().Unix(), 10)
}
for i := 0; i < limit/100; i++ {
go func() {
var names []string
for j := 0; j < limit/3; j++ {
length := 2 + rand.Intn(2)
if length == 2 {
name := appg.Name.FamilyNames[utils.RandInt(0, len(appg.Name.FamilyNames))] +
appg.Name.AfterNames[utils.RandInt(0, len(appg.Name.AfterNames))]
names = append(names, name)
}
if length == 3 {
name := appg.Name.FamilyNames[utils.RandInt(0, len(appg.Name.FamilyNames))] +
appg.Name.MiddleNames[utils.RandInt(0, len(appg.Name.MiddleNames))] +
appg.Name.AfterNames[utils.RandInt(0, len(appg.Name.AfterNames))]
names = append(names, name)
}
}
_ = redis.SAdd(redis.NameSource, names)
}()
time.Sleep(100 * time.Millisecond)
}
_ = redis.Expire(redis.NameSource, redis.NameSourceExpire)
}
name, err := redis.SPop(redis.NameSource)
if name == "" || err != nil {
return "游客" + strconv.FormatInt(time.Now().Unix(), 10)
}
return name
}