Initial commit

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 11:07:42 +08:00
co-authored by Claude Opus 5.5
commit 7974a42301
633 changed files with 120248 additions and 0 deletions
+478
View File
@@ -0,0 +1,478 @@
package authser
import (
"context"
"strings"
"time"
"videoweb/app/appg"
"videoweb/app/service/reportser"
"videoweb/pub/common/enum/consts"
"videoweb/pub/common/log"
"videoweb/pub/common/srvdef"
"videoweb/pub/common/useragent"
"videoweb/pub/common/utils"
"videoweb/pub/models/usermod"
"videoweb/pub/mongos"
"videoweb/pub/pubdef"
"go.mongodb.org/mongo-driver/bson"
"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 := strings.ToUpper(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 strings.ToUpper(utils.GetRandomLetters(12))
}
// LoginResp 登录/注册成功后的响应数据
type LoginResp struct {
Token string `json:"token"`
UserId int64 `json:"userId"`
AccountNo string `json:"accountNo"`
Email string `json:"email"`
NickName string `json:"nickName"`
Introduction string `json:"introduction"` // 简介
VipType pubdef.VipType `json:"vipType"`
CurrentVipExpire time.Time `json:"currentVipExpire"` // VIP 过期时间,客户端需结合 vipType 判断是否真正有效
ShowCardId int64 `json:"showCardId"` // 最新会员卡id
ShowCardName string `json:"showCardName"` // 最新会员卡名称
Status pubdef.UserStatus `json:"status"`
Avatar string `json:"avatar"` // 头像
InviteCode string `json:"inviteCode"` // 邀请码
Type pubdef.UserType `json:"type"` // 身份类型:0游客 1普通用户
LoginType pubdef.LoginType `json:"loginType"` // 登陆方式
IsRegister bool `json:"isRegister"` // 是否已注册
IsNewUser bool `json:"isNewUser"` // 本次是否新建游客
PrivacyMode bool `json:"privacyMode"` // 隐私模式
Score int64 `json:"score"` // 积分
Recharge int64 `json:"recharge"` // 金币余额
}
// 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
}
// generateCrawlerToken 签发爬虫观看 JWT,不写入 Redis,避免占用真实用户槽位。
func generateCrawlerToken() (string, *srvdef.SrvErr) {
token, err := utils.GenerateUserToken(utils.UserClaims{
UserID: pubdef.CrawlerUserId,
Expire: consts.UserJwtTokenExpire,
}, appg.Conf.JwtKey)
if err != nil {
return "", srvdef.New(srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg(), "")
}
return token, nil
}
// CrawlerLoginResp 爬虫虚拟游客资料,不对应任何数据库用户。
func CrawlerLoginResp(token string) *LoginResp {
return &LoginResp{
Token: token,
UserId: pubdef.CrawlerUserId,
NickName: "游客",
Status: pubdef.UserStatusNormal,
Type: pubdef.UserTypeGuest,
LoginType: pubdef.LoginTypeDevice,
IsRegister: false,
IsNewUser: false,
}
}
// Register 用户注册
// guestUserId:请求头 Authorization 解析出的用户ID;若为游客则升级该用户,否则新建
// agentInviteCode:邀请人的邀请码,可为空
func Register(ctx context.Context, guestUserId int64, accountNo, email, password, agentInviteCode string, ua useragent.UA, ip, channel string) (*LoginResp, *srvdef.SrvErr) {
accountNo = strings.TrimSpace(accountNo)
email = strings.TrimSpace(email)
password = strings.TrimSpace(password)
// 检查账号是否已存在
exist, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: accountNo})
if exist != nil {
return nil, srvdef.New(srvdef.ErrDbInputExist, "账号已存在", "")
}
// 检查邮箱是否已存在
exist, _ = usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Email: email})
if exist != nil {
return nil, srvdef.New(srvdef.ErrDbInputExist, "邮箱已被注册", "")
}
hashed, err := hashPassword(password)
if err != nil {
return nil, srvdef.New(srvdef.Failure, "密码处理失败", "")
}
now := time.Now()
// 请求头带游客 token:升级当前游客为普通用户
if guestUserId > 0 {
guest, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: guestUserId})
if serr == nil && isGuestUser(guest) {
setDoc := bson.M{
usermod.FN.User.AccountNo: accountNo,
usermod.FN.User.Email: email,
usermod.FN.User.Password: hashed,
usermod.FN.User.Type: pubdef.UserTypeNormal,
usermod.FN.User.IsRegister: true,
usermod.FN.User.LoginType: pubdef.LoginTypeAccount,
usermod.FN.User.RegisterTime: now,
usermod.FN.User.LastLoginAt: now,
usermod.FN.User.UpdatedAt: now,
usermod.FN.User.LastIp: ip,
}
if agentInviteCode != "" && guest.AgentInviteCode == "" {
setDoc[usermod.FN.User.AgentInviteCode] = agentInviteCode
setDoc[usermod.FN.User.IsDirect] = false
guest.IsDirect = false
}
if v := strings.TrimSpace(ua.DevID); v != "" {
setDoc[usermod.FN.User.DevID] = v
}
if v := strings.TrimSpace(ua.DevType); v != "" {
setDoc[usermod.FN.User.Device] = v
}
if v := strings.TrimSpace(ua.SysType); v != "" {
setDoc[usermod.FN.User.SysType] = v
}
if v := strings.TrimSpace(ua.Ver); v != "" {
setDoc[usermod.FN.User.AppVer] = v
}
mergeUAExtFieldsToSet(setDoc, guest, ua)
applyChannelIfMissing(setDoc, guest, channel)
if serr = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: guest.Id}, bson.M{"$set": setDoc}); serr != nil {
return nil, serr
}
if agentInviteCode != "" && guest.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: now},
},
)
}
guest.AccountNo = accountNo
guest.Email = email
guest.Type = pubdef.UserTypeNormal
guest.IsRegister = true
guest.LoginType = pubdef.LoginTypeAccount
guest.RegisterTime = now
token, serr := generateAndSaveToken(guest.Id)
if serr != nil {
return nil, serr
}
reportser.ReportUserRegister(guest, ua, ip, "username")
reportser.ReportUserLogin(guest, ua, ip, pubdef.LoginTypeAccount)
return toLoginResp(ctx, guest, token, true), nil
}
}
user := &usermod.User{
Id: mongos.Lid.User.Get(),
AccountNo: accountNo,
Email: email,
Password: hashed,
NickName: appg.GenerateNickname(),
Avatar: randomAvatar(),
InviteCode: generateUniqueInviteCode(ctx),
AgentInviteCode: agentInviteCode,
Type: pubdef.UserTypeNormal,
Status: pubdef.UserStatusNormal,
IsRegister: true,
LoginType: pubdef.LoginTypeAccount,
RegIp: ip,
LastIp: ip,
DevID: ua.DevID,
Device: ua.DevType,
SysType: ua.SysType,
AppVer: ua.Ver,
OriVer: ua.Ver,
SessionID: ua.SessionID,
DeviceBrand: ua.DeviceBrand,
DeviceModel: ua.DeviceModel,
SystemName: ua.SystemName,
SystemVersion: ua.SystemVersion,
DistrictCode: normalizeChannel(channel),
IsDirect: resolveUserDistrict(channel, agentInviteCode, ip, ua.SysType).IsDirect,
LastLoginAt: now,
RegisterTime: now,
CreatedAt: now,
UpdatedAt: now,
}
if serr := usermod.InsertOneUser(ctx, user); serr != nil {
return nil, serr
}
// 若填写了邀请码,给对应用户的 invites +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
}
reportser.ReportUserRegister(user, ua, ip, "username")
reportser.ReportUserLogin(user, ua, ip, pubdef.LoginTypeAccount)
return toLoginResp(ctx, user, token, false), nil
}
// Login 用户登录(账号或邮箱均可)
func Login(ctx context.Context, account, password string, ua useragent.UA, ip, channel string) (*LoginResp, *srvdef.SrvErr) {
// 账号或邮箱均可登录
user, _ := usermod.FindOneUser(ctx, bson.M{
"$or": []bson.M{
{usermod.FN.User.AccountNo: account},
{usermod.FN.User.Email: 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
}
now := time.Now()
setDoc := bson.M{
usermod.FN.User.LastLoginAt: now,
usermod.FN.User.UpdatedAt: now,
}
if ip != "" {
setDoc[usermod.FN.User.LastIp] = ip
}
if v := strings.TrimSpace(ua.DevID); v != "" {
setDoc[usermod.FN.User.DevID] = v
}
if v := strings.TrimSpace(ua.DevType); v != "" {
setDoc[usermod.FN.User.Device] = v
}
if v := strings.TrimSpace(ua.SysType); v != "" {
setDoc[usermod.FN.User.SysType] = v
}
if v := strings.TrimSpace(ua.Ver); v != "" {
setDoc[usermod.FN.User.AppVer] = v
}
// 已有账号的历史用户统一修正为普通用户
if strings.TrimSpace(user.AccountNo) != "" && user.Type != pubdef.UserTypeNormal {
setDoc[usermod.FN.User.Type] = pubdef.UserTypeNormal
setDoc[usermod.FN.User.IsRegister] = true
user.Type = pubdef.UserTypeNormal
user.IsRegister = true
}
mergeUAExtFieldsToSet(setDoc, user, ua)
applyChannelIfMissing(setDoc, user, channel)
_ = usermod.UpdateOneUser(ctx,
bson.M{usermod.FN.User.Id: user.Id},
bson.M{"$set": setDoc},
)
reportser.ReportUserLogin(user, ua, ip, pubdef.LoginTypeAccount)
return toLoginResp(ctx, user, token, true), nil
}
// GetInfo 获取当前登录用户的最新信息(不刷新 token)
func GetInfo(ctx context.Context, userId int64, ua useragent.UA) (*LoginResp, *srvdef.SrvErr) {
if pubdef.IsCrawlerUser(userId) {
return CrawlerLoginResp(""), nil
}
user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
if user == nil {
return nil, srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "")
}
now := time.Now()
setDoc := bson.M{}
if v := strings.TrimSpace(ua.DevID); v != "" && v != user.DevID {
setDoc[usermod.FN.User.DevID] = v
}
if v := strings.TrimSpace(ua.DevType); v != "" && v != user.Device {
setDoc[usermod.FN.User.Device] = v
}
if v := strings.TrimSpace(ua.SysType); v != "" && v != user.SysType {
setDoc[usermod.FN.User.SysType] = v
}
if v := strings.TrimSpace(ua.Ver); v != "" && v != user.AppVer {
setDoc[usermod.FN.User.AppVer] = v
}
if strings.TrimSpace(user.AccountNo) != "" && user.Type != pubdef.UserTypeNormal {
setDoc[usermod.FN.User.Type] = pubdef.UserTypeNormal
setDoc[usermod.FN.User.IsRegister] = true
user.Type = pubdef.UserTypeNormal
user.IsRegister = true
}
mergeUAExtFieldsToSet(setDoc, user, ua)
if len(setDoc) > 0 {
setDoc[usermod.FN.User.UpdatedAt] = now
_ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": setDoc})
}
return toLoginResp(ctx, user, "", true), nil
}
func mergeUAExtFieldsToSet(setDoc bson.M, user *usermod.User, ua useragent.UA) {
if user == nil {
return
}
if v := strings.TrimSpace(ua.SessionID); v != "" && v != user.SessionID {
setDoc[usermod.FN.User.SessionID] = v
}
if v := strings.TrimSpace(ua.DeviceBrand); v != "" && v != user.DeviceBrand {
setDoc[usermod.FN.User.DeviceBrand] = v
}
if v := strings.TrimSpace(ua.DeviceModel); v != "" && v != user.DeviceModel {
setDoc[usermod.FN.User.DeviceModel] = v
}
if v := strings.TrimSpace(ua.SystemName); v != "" && v != user.SystemName {
setDoc[usermod.FN.User.SystemName] = v
}
if v := strings.TrimSpace(ua.SystemVersion); v != "" && v != user.SystemVersion {
setDoc[usermod.FN.User.SystemVersion] = v
}
}
// UpdateProfileReq 修改用户资料的参数(所有字段均为可选)
type UpdateProfileReq struct {
UserId int64 // 用户ID(从 token 中获取)
NickName *string // 新昵称(可选)
Avatar *string // 新头像 URL(可选)
Introduction *string // 简介(可选)
PrivacyMode *bool // 隐私模式:true-开启,false-关闭(可选)
OldPassword *string // 旧密码明文(修改密码时必传)
NewPassword *string // 新密码明文(修改密码时必传)
}
// UpdateProfile 修改用户资料:支持修改头像、简介、密码,或组合修改
func UpdateProfile(ctx context.Context, req *UpdateProfileReq) *srvdef.SrvErr {
user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: req.UserId})
if user == nil {
return srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "")
}
setDoc := bson.M{usermod.FN.User.UpdatedAt: time.Now()}
if req.NickName != nil && *req.NickName != "" {
setDoc[usermod.FN.User.NickName] = *req.NickName
}
if req.Avatar != nil && *req.Avatar != "" {
setDoc[usermod.FN.User.Avatar] = *req.Avatar
}
if req.Introduction != nil {
setDoc[usermod.FN.User.Introduction] = strings.TrimSpace(*req.Introduction)
}
if req.PrivacyMode != nil {
setDoc[usermod.FN.User.PrivacyMode] = *req.PrivacyMode
}
// 修改密码:两者必须同时提供
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},
)
}
// ResetPassword 通过账号+邮箱双重校验后重置密码
func ResetPassword(ctx context.Context, accountNo, email, newPassword string) *srvdef.SrvErr {
user, _ := usermod.FindOneUser(ctx, bson.M{
usermod.FN.User.AccountNo: accountNo,
usermod.FN.User.Email: email,
})
if user == nil {
return srvdef.New(srvdef.ErrUserNotExsit, "账号或邮箱不匹配", "")
}
hashed, err := hashPassword(newPassword)
if err != nil {
return srvdef.New(srvdef.Failure, "密码处理失败", "")
}
return usermod.UpdateOneUser(ctx,
bson.M{usermod.FN.User.Id: user.Id},
bson.M{"$set": bson.M{
usermod.FN.User.Password: hashed,
usermod.FN.User.UpdatedAt: time.Now(),
}},
)
}
+68
View File
@@ -0,0 +1,68 @@
package authser
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"videoweb/app/appg"
"videoweb/pub/common/log"
"videoweb/pub/common/statcenter"
"videoweb/pub/common/utils"
)
// UserDistrictInfo 统计中心返回的渠道/直推信息
type UserDistrictInfo struct {
IsDirect bool `json:"isDirect"`
DistrictCode string `json:"districtCode"`
ParentPromCode string `json:"parentPromCode"`
ParentId uint64 `json:"parentId"`
ParentInviteCount int64 `json:"parentInviteCount"`
}
type userDistrictReq struct {
AppId int32 `json:"appId"`
DistrictCode string `json:"dc"`
PromSeqe string `json:"pc"`
IP string `json:"ip"`
SysType string `json:"sysType"`
}
// resolveUserDistrict 有邀请码视为非直推;否则向统计中心查询补量/直推。
func resolveUserDistrict(channel, inviteCode, ip, sysType string) UserDistrictInfo {
inviteCode = strings.TrimSpace(inviteCode)
if inviteCode != "" {
return UserDistrictInfo{IsDirect: false, ParentPromCode: inviteCode}
}
return GetUserDistictInfo(channel, inviteCode, ip, sysType)
}
// GetUserDistictInfo 查询统计中心用户渠道信息,对齐 ksx_server。
func GetUserDistictInfo(dc, pc, ip, sysType string) (districtInfo UserDistrictInfo) {
base := strings.TrimSpace(appg.Conf.NewProductUrl)
if base == "" {
return
}
request := userDistrictReq{
AppId: statcenter.STAT_APPID,
SysType: sysType,
DistrictCode: dc,
PromSeqe: pc,
IP: ip,
}
bodyStr, err := json.Marshal(request)
if err != nil {
log.Info(fmt.Sprintf("GetUserDistictInfo json.Marshal is fail error:%+v/data:%+v", err, request))
return
}
url := strings.TrimRight(base, "/") + "/api/stat/userInvite/district"
code, err := utils.POSTWithJResp(&districtInfo, url, nil, bodyStr)
if err != nil {
log.Error("GetUserDistictInfo POSTWithJResp", log.Any("url", url), log.E(err))
return
}
if code != http.StatusOK {
log.Error("GetUserDistictInfo response status", log.Any("code", code))
}
return
}
+276
View File
@@ -0,0 +1,276 @@
package authser
import (
"context"
"fmt"
"strings"
"time"
"videoweb/app/appg"
"videoweb/app/middleware/reptilerecord"
"videoweb/app/middleware/visit"
"videoweb/app/service/configser"
"videoweb/app/service/reportser"
"videoweb/pub/common/log"
"videoweb/pub/common/netutil"
"videoweb/pub/common/srvdef"
"videoweb/pub/common/useragent"
"videoweb/pub/models/usermod"
"videoweb/pub/mongos"
"videoweb/pub/pubdef"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/bson"
)
// GuestLoginReq 游客登录请求
type GuestLoginReq struct {
DevID string `json:"devID"` // 设备唯一标示(必填)
Mac string `json:"mac"` // 设备 Mac(可选)
DeviceId string `json:"deviceId"` // 备用设备ID(可选,写入 DevV2)
Channel string `json:"channel"` // 渠道码(可选,用户无商区码时写入 DistrictCode)
}
// isGuestUser 是否为可升级的游客身份
func isGuestUser(user *usermod.User) bool {
if user == nil {
return false
}
if user.Type == pubdef.UserTypeNormal {
return false
}
return !user.IsRegister && strings.TrimSpace(user.AccountNo) == ""
}
// GuestLogin 游客登录:按 token / DevID / Mac / DeviceId 查找,不存在则创建游客
func GuestLogin(ctx context.Context, userId int64, ip string, req *GuestLoginReq, ua useragent.UA) (*LoginResp, *srvdef.SrvErr) {
if isBotRequest(ctx) {
return crawlerGuestLogin()
}
if req == nil || strings.TrimSpace(req.DevID) == "" {
return nil, srvdef.New(srvdef.ErrParamError, "devID不能为空", "")
}
ua.DevID = strings.TrimSpace(req.DevID)
var user *usermod.User
var serr *srvdef.SrvErr
reuseToken := false
if userId > 0 {
user, serr = usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
if serr != nil {
if serr.Code != srvdef.ErrUserNotExsit {
return nil, serr
}
userId = 0
user = nil
} else {
reuseToken = true
}
}
isNewUser := false
if userId == 0 {
user, serr = findGuestUser(ctx, req)
if serr != nil {
if serr.Code != srvdef.ErrUserNotExsit {
return nil, serr
}
user, serr = createGuestUser(ctx, ua, req, ip)
if serr != nil {
return nil, serr
}
isNewUser = true
}
}
if user.Status == pubdef.UserStatusDisable {
return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
}
now := time.Now()
setDoc := bson.M{
usermod.FN.User.LastLoginAt: now,
usermod.FN.User.UpdatedAt: now,
usermod.FN.User.LastIp: ip,
usermod.FN.User.LoginType: pubdef.LoginTypeDevice,
}
if v := strings.TrimSpace(ua.DevID); v != "" {
setDoc[usermod.FN.User.DevID] = v
}
if v := strings.TrimSpace(ua.DevType); v != "" {
setDoc[usermod.FN.User.Device] = v
}
if v := strings.TrimSpace(ua.SysType); v != "" {
setDoc[usermod.FN.User.SysType] = v
}
if v := strings.TrimSpace(ua.Ver); v != "" {
setDoc[usermod.FN.User.AppVer] = v
}
if v := strings.TrimSpace(req.Mac); v != "" {
setDoc[usermod.FN.User.Mac] = v
}
if v := strings.TrimSpace(req.DeviceId); v != "" {
setDoc[usermod.FN.User.DevV2] = v
}
// 历史用户 Type 为空/旧值时,未注册则归为游客
if isGuestUser(user) && user.Type != pubdef.UserTypeGuest {
setDoc[usermod.FN.User.Type] = pubdef.UserTypeGuest
user.Type = pubdef.UserTypeGuest
}
mergeUAExtFieldsToSet(setDoc, user, ua)
applyChannelIfMissing(setDoc, user, req.Channel)
_ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": setDoc})
var token string
if reuseToken {
if old, err := tokener.Get(user.Id); err == nil && len(old) > 0 {
token = string(old)
}
}
if token == "" {
token, serr = generateAndSaveToken(user.Id)
if serr != nil {
return nil, serr
}
}
resp := toLoginResp(ctx, user, token, true)
resp.IsNewUser = isNewUser
if isNewUser {
visit.RecordDailyVisit(ctx, user.Id, ua, ip)
reportser.ReportUserRegister(user, ua, ip, "deviceid")
}
reportser.ReportUserLogin(user, ua, ip, pubdef.LoginTypeDevice)
return resp, nil
}
func findGuestUser(ctx context.Context, req *GuestLoginReq) (*usermod.User, *srvdef.SrvErr) {
user, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevID: req.DevID})
if serr == nil {
return user, nil
}
if serr.Code != srvdef.ErrUserNotExsit {
return nil, serr
}
mac := strings.TrimSpace(req.Mac)
if mac != "" && !strings.Contains(mac, "00:00:00:00") {
user, serr = usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Mac: mac})
if serr == nil {
return user, nil
}
if serr.Code != srvdef.ErrUserNotExsit {
return nil, serr
}
}
deviceId := strings.TrimSpace(req.DeviceId)
if deviceId != "" && !strings.Contains(deviceId, "00:00:00:00") {
return usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevV2: deviceId})
}
return nil, srvdef.New(srvdef.ErrUserNotExsit, "", "")
}
func createGuestUser(ctx context.Context, ua useragent.UA, req *GuestLoginReq, ip string) (*usermod.User, *srvdef.SrvErr) {
now := time.Now()
uid := mongos.Lid.User.Get()
districtInfo := resolveUserDistrict(req.Channel, "", ip, ua.SysType)
user := &usermod.User{
Id: uid,
Type: pubdef.UserTypeGuest,
Status: pubdef.UserStatusNormal,
LoginType: pubdef.LoginTypeDevice,
IsRegister: false,
NickName: fmt.Sprintf("游客%d", uid),
Avatar: randomAvatar(),
InviteCode: generateUniqueInviteCode(ctx),
DistrictCode: normalizeChannel(req.Channel),
IsDirect: districtInfo.IsDirect,
RegIp: ip,
LastIp: ip,
DevID: ua.DevID,
Mac: strings.TrimSpace(req.Mac),
DevV2: strings.TrimSpace(req.DeviceId),
Device: ua.DevType,
SysType: ua.SysType,
AppVer: ua.Ver,
OriVer: ua.Ver,
SessionID: ua.SessionID,
DeviceBrand: ua.DeviceBrand,
DeviceModel: ua.DeviceModel,
SystemName: ua.SystemName,
SystemVersion: ua.SystemVersion,
LastLoginAt: now,
CreatedAt: now,
UpdatedAt: now,
}
if nick := appg.GenerateNickname(); nick != "" {
user.NickName = nick
}
if serr := usermod.InsertOneUser(ctx, user); serr != nil {
log.Error("authser:createGuestUser InsertOneUser failed", log.E(serr), log.Any("devID", ua.DevID))
return nil, serr
}
return user, nil
}
func toLoginResp(ctx context.Context, user *usermod.User, token string, withCDN bool) *LoginResp {
avatar := user.Avatar
if withCDN {
cdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
avatar = netutil.MergeURL(cdn, user.Avatar)
}
return &LoginResp{
Token: token,
UserId: user.Id,
AccountNo: user.AccountNo,
Email: user.Email,
NickName: user.NickName,
Introduction: user.Introduction,
VipType: user.VipType,
CurrentVipExpire: user.CurrentVipExpire,
ShowCardId: user.ShowCardId,
ShowCardName: user.ShowCardName,
Status: user.Status,
Avatar: avatar,
InviteCode: user.InviteCode,
Type: user.Type,
LoginType: user.LoginType,
IsRegister: user.IsRegister,
PrivacyMode: user.PrivacyMode,
Score: user.Score,
Recharge: user.Recharge,
}
}
func normalizeChannel(channel string) string {
return strings.TrimSpace(channel)
}
func hasDistrictCode(code string) bool {
return strings.TrimSpace(code) != ""
}
func isBotRequest(ctx context.Context) bool {
gc, ok := ctx.(*gin.Context)
return ok && reptilerecord.IsBot(gc)
}
// crawlerGuestLogin 给爬虫签发可观看的虚拟游客 token,不建用户、不写日活、不上报。
func crawlerGuestLogin() (*LoginResp, *srvdef.SrvErr) {
token, serr := generateCrawlerToken()
if serr != nil {
return nil, serr
}
return CrawlerLoginResp(token), nil
}
func applyChannelIfMissing(setDoc bson.M, user *usermod.User, channel string) {
if user == nil || hasDistrictCode(user.DistrictCode) {
return
}
if ch := normalizeChannel(channel); ch != "" {
setDoc[usermod.FN.User.DistrictCode] = ch
user.DistrictCode = ch
}
}