package userser import ( "91porn-server/app/appg" "91porn-server/app/middleware/authuser" "91porn-server/app/service/adser" "91porn-server/app/service/imser" "91porn-server/app/service/mediaser" "91porn-server/app/service/messageser" "91porn-server/app/service/notiser" "91porn-server/app/service/productser" "91porn-server/app/service/proxyser" "91porn-server/app/service/sys_config" "91porn-server/app/service/taskser" "91porn-server/app/service/vidhelpser" "91porn-server/app/service/walletser" "91porn-server/common" "91porn-server/common/constant" "91porn-server/common/constant/redisconst" "91porn-server/common/crypt" "91porn-server/common/datacenter" "91porn-server/common/db" "91porn-server/common/log" sli "91porn-server/common/slice" "91porn-server/common/stderr" "91porn-server/common/timeutil" "91porn-server/common/timeutil/timerange" "91porn-server/common/version" "91porn-server/common/ysqr" "91porn-server/middleware/ua" "91porn-server/models/cache/aiplazadata" "91porn-server/models/cache/sysconfdata" "91porn-server/models/commod" "91porn-server/models/l/exchlogmod" "91porn-server/models/l/payvidlgmod" "91porn-server/models/s/statrecordmod" "91porn-server/models/v/aiplazamod" "91porn-server/models/v/dailytaskmod" "91porn-server/models/v/exchcodemod" "91porn-server/models/v/faqmod" "91porn-server/models/v/followmod" "91porn-server/models/v/idmod" "91porn-server/models/v/ipblockmod" "91porn-server/models/v/likemod" "91porn-server/models/v/mediamod" "91porn-server/models/v/oncetaskmod" "91porn-server/models/v/prdcthsomod" "91porn-server/models/v/productmod" "91porn-server/models/v/proxymod" "91porn-server/models/v/signrecordmod" "91porn-server/models/v/sourcemod" "91porn-server/models/v/sysconfmod" "91porn-server/models/v/taskmod" "91porn-server/models/v/txnmod" "91porn-server/models/v/usermod" "91porn-server/models/v/videocoupon" "91porn-server/models/v/videodiscountmod" "91porn-server/models/v/vidmod" "91porn-server/models/v/walletmod" "context" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "fmt" "math" "math/rand" "strconv" "strings" "sync" "time" "github.com/go-redis/redis" "github.com/shopspring/decimal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo/options" ) const firstBindPhoneGold = 3 func CheckDevIDSign(devID, devToken string) bool { if devID == "" || devToken == "" { return false } s := appg.Conf.Base.DevIDSignKey + devID + appg.Conf.Base.DevIDSignKey sha := sha256.Sum256([]byte(s)) bsha := base64.StdEncoding.EncodeToString(sha[:]) return bsha == devToken } // Register 游客注册 func Register(dc, pc, puc, devID, ip, sysType, devToken, devType, mobile, ver, terminal string, ua ua.UA, tid string) (uid uint64, token string, err error) { if devType == "aosp:25" { //疑似刷子使用的devType,正常用户已无,封禁注册 err = errors.New("invalid") return } uid, err = idmod.GetNextUID() if err != nil { return } //默认用户类型 userType := usermod.UserUnkown if terminal == constant.TerminalWeb { //web默认注册为游客 userType = usermod.UserTourists } token, err = authuser.GenToken(&authuser.Claims{UID: uid, Timestamp: time.Now().UnixNano(), Type: uint8(userType)}) if err != nil { return } sysType = strings.ToLower(sysType) newPromCode, err := makeOutPromotionCode() if err != nil { return } portrait := GetPortrait() //设置默认头像 name := getName() // 广告用户分组 adGroup := commod.AdGroupNone configure, _ := sysconfdata.GetAllFromCache() adverAbTestSwitch := configure.GetBool(sysconfmod.VCodeAdverAbTestSwitch) if adverAbTestSwitch && (appg.Conf.Base.Env == "test" || (ua.SysType == constant.SysTypeAndroid && adser.VersionCompare(ver, constant.AdGroupClientVersion))) { switch uid % 3 { case 0: adGroup = commod.AdGroupA case 1: adGroup = commod.AdGroupB default: adGroup = commod.AdGroupC } } // TODO:根据dc和pc 查询渠道并返回全民代理关系。 districtInfo := GetUserDistictInfo(dc, pc, ip, sysType) u := usermod.User{ UID: uid, DevID: devID, Name: name, Portrait: portrait, RegisterIP: ip, WatchCount: sys_config.GetTotalWatchCount(), VideoDeduction: 3, //默认视频扣量 30% Token: token, PromCode: newPromCode, DevToken: devToken, VipExpireDate: time.Unix(0, 0), DevType: devType, SysType: common.HandleSysType(sysType), Mobile: mobile, Ver: ver, PUC: puc, DiscDoc: commod.NewDiscDoc(getDiscSeqe(districtInfo.DistrictCode, pc), districtInfo.IsDirect, time.Now()), LastSysType: common.HandleSysType(sysType), Type: userType, Tid: tid, AdGroup: adGroup, OriVer: ver, } if err = usermod.InsertUser(&u); err != nil { return } if mobile != "" { go func() { _ = taskser.CompleteOnceTask(nil, uid, oncetaskmod.OnceTaskTypeBindMobile) }() } if districtInfo.ParentPromCode != "" { authPromotionCode(uid, &districtInfo) } //Device: ua.SysType, // DeviceID: CutTo50(ua.DevID), // UserAgent: ua.UserAgent, // DeviceModel: ua.DevType, if terminal != constant.TerminalWeb { // 数据中心埋点 userMsg := &datacenter.UserRegisterMsg{ UserId: int64(u.UID), AppId: commod.KFK_APPID, SysType: u.SysType, DevType: u.DevType, Name: u.Name, DeviceId: u.DevID, IP: ip, DistrictCode: u.DistrictCode, InviteCode: districtInfo.ParentPromCode, RegisterAt: time.Now(), UserAgent: ua.BuildID, } datacenter.AsyncSendMessage(datacenter.TopicUserRegister, userMsg.Marshal()) } redisRegister(uid, sysType, time.Now()) return } func RandName() string { return getName() } func getName() string { const limit = 300 totalCnt, err := appg.Redis.SCard(redisconst.NameSource) if err != nil || totalCnt < limit { if len(appg.Name.FamilyNames) == 0 || len(appg.Name.AfterNames) == 0 { return "游客" + strconv.FormatInt(time.Now().Unix(), 10) } for i := 0; i < limit/100; i++ { common.Go(func() { var names []string for j := 0; j < limit/3; j++ { length := common.RandInt(2, 4) if length == 2 { name := appg.Name.FamilyNames[common.RandInt(0, len(appg.Name.FamilyNames))] + appg.Name.AfterNames[common.RandInt(0, len(appg.Name.AfterNames))] names = append(names, name) } if length == 3 { name := appg.Name.FamilyNames[common.RandInt(0, len(appg.Name.FamilyNames))] + appg.Name.MiddleNames[common.RandInt(0, len(appg.Name.MiddleNames))] + appg.Name.AfterNames[common.RandInt(0, len(appg.Name.AfterNames))] names = append(names, name) } } _, _ = appg.Redis.SAdd(redisconst.NameSource, names) }) time.Sleep(100 * time.Millisecond) } _, _ = appg.Redis.ExpireKey(redisconst.NameSource, redisconst.NameSourceExpire) } name, err := appg.Redis.SPop(redisconst.NameSource) if name == "" || err != nil { return "游客" + strconv.FormatInt(time.Now().Unix(), 10) } return name } func GetPortrait() string { var portrait string if portraitLen := len(appg.Static.Portrait); portraitLen > 0 { portrait = appg.Static.Portrait[common.RandInt(0, portraitLen)] } return portrait } func getDiscSeqe(dc, pc string) commod.DiscSeqe { var ds commod.DiscSeqe if dc != "" { ds.From(dc) } ds.PromSeqe = pc return ds } func redisRegister(uid uint64, sysType string, now time.Time) { common.Go(func() { //需要使用 common.Go 以监视携程 recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) rbk := redisconst.RegistBehaviorKey(uid) ntf := redisconst.NotifyKey(uid) redisKey := redisconst.RegistKey(recentMinute) _, _ = appg.Redis.Piplined(func(pipeliner redis.Pipeliner) error { pipeliner.SAdd(redisKey, usermod.RegistValue{UID: uid, SysType: sysType}) pipeliner.Expire(redisKey, redisconst.RegistExpireMax) return nil }) _, _ = appg.Redis.Piplined(func(pipeliner redis.Pipeliner) error { pipeliner.HMSet(rbk, map[string]interface{}{"default": 1}) pipeliner.HMSet(ntf, map[string]interface{}{"default": 1}) pipeliner.Expire(ntf, redisconst.NotifyKeyExpire) pipeliner.Expire(rbk, redisconst.NewRegisterBehaExpire) return nil }) }) } // 验证推广码 func authPromotionCode(uid uint64, inviteInfo *UserDistrictInfo) bool { code := proxyser.InviteBind(uid, inviteInfo.ParentPromCode, false) // fmt.Println("***authPromotionCode", code) log.Info("authPromotionCode", log.Any("uid", uid), log.Any("code", code), log.Any("ParentPromCode", inviteInfo.ParentPromCode), log.Any("ParentInviteCount", inviteInfo.ParentInviteCount)) return code == stderr.Success } // 获取推广吗 func makeOutPromotionCode() (string, error) { redisKey := redisconst.PromotionCodeKey() p, _ := appg.Redis.SPop(redisKey) if p != "" { return p, nil } return getPromotionCode() } // 获取不重复的推广码 func getPromotionCode() (string, error) { promotionCode := common.InvitePromotionCodeGenera() u, err := usermod.FindUserPromotionCode(promotionCode) if err != nil { return "", err } if u == nil { return promotionCode, nil } return getPromotionCode() } // MobileBind 绑定手机号码 func MobileBind(uid uint64, mobile, passWord string) stderr.Code { //手机号是否绑定 user, err := usermod.FindUserByMobile(mobile) if err != nil { return stderr.Failure } if user != nil { if user.UID == uid { return stderr.ErrMobileHasBind } return stderr.ErrMobileHasBindByOther } if err = updateUserMobile(uid, mobile, passWord); err != nil { log.Warn("updateUserMobile error", log.E(err)) return stderr.ErrNetWorkBusy } go func() { _ = taskser.CompleteOnceTask(nil, uid, oncetaskmod.OnceTaskTypeBindMobile) }() return stderr.Success } // MobileBind 手机号码登陆 func MobileLoginOnly(ip, mobile string, ua ua.UA) (*usermod.UserRes, stderr.Code) { //手机号是否绑定 user, err := usermod.FindUserByMobile(mobile) if err != nil { return nil, stderr.ErrNetWorkBusy } if user == nil { return nil, stderr.ErrLoginMobileNotExist } //对马甲账号的合法性做判断,不合法直接视为封禁 if !IsMaJiaAccount(mobile, user.DevID, user.UID) { return nil, stderr.ErrAccessForbid } if user.HasLocked { return &usermod.UserRes{User: *user}, stderr.ErrAccessForbid } //生产token 刷新token token, _ := authuser.GenToken(&authuser.Claims{UID: user.UID, Timestamp: time.Now().UnixNano(), Type: uint8(user.Type)}) if _, err = usermod.UpdateUserToken(user.UID, token); err != nil { return nil, stderr.ErrNetWorkBusy } userInfo, err := Info(user.UID) if err != nil { return nil, stderr.ErrNetWorkBusy } userInfo.City = vidhelpser.GetLocationByIP(ip) userInfo.Token = token authuser.RevokeTokenCache(user.UID) return &userInfo, stderr.Success } func lastLogin(uid uint64, token, sys string) error { _, err := usermod.Update(uid, usermod.UserSelector{ Token: &token, LastSysType: &sys, }) return err } // 默认为都为非马甲账户 不经过校验,只有是马甲账户时才校验合法, func IsMaJiaAccount(mobile, devId string, uid uint64) bool { struid := strconv.FormatUint(uid, 10) return !(mobile[:len(constant.FakeMobilePrefix)] == constant.FakeMobilePrefix && devId != (usermod.SystemDevIDPrex+struid)) } func updateUserAccount(uid, newUid uint64, account, password string) error { if account == "" { return errors.New("empty user account") } user, err := usermod.FindUserByUID(uid) if err != nil { return err } if user.Account == account { return nil } if user.AccountBindAt == nil { devID := "web-" + user.DevID ok, err := usermod.BindAccount(uid, newUid, account, password, devID) if err != nil { return err } if !ok { return errors.New("bind account failed") } } return nil } func updateUserMobile(uid uint64, mobile, passWord string) error { if mobile == "" { return errors.New("mobile is empty") } newUser, err := usermod.FindUserByUID(uid) if err != nil { return err } if newUser.Mobile == mobile { return nil } if newUser.MobileBindAt == nil { //首次绑定 return appg.VideoDB.Trans(func(t *db.MongoTool) error { ok, err := usermod.BindMobile(t, uid, mobile, passWord) if err != nil { return err } if !ok { return errors.New("mobile bind faild") } tl := txnmod.TransactionLog{ TransNo: primitive.NewObjectID(), UID: uid, TranType: txnmod.TranType_WLSysGive.Key(), TranTypeInt: int64(txnmod.TranType_WLSysGive), Desc: fmt.Sprintf("绑定手机赠送: %s游戏金币", strconv.Itoa(firstBindPhoneGold)), DiscDoc: newUser.DiscDoc, SysType: newUser.SysType, Money: decimal.NewFromInt(firstBindPhoneGold), } return txnmod.InsertTransactionLog(t, &tl) }) } ok, err := usermod.ChangeMobile(uid, mobile) //更换手机号 if err != nil { return err } if !ok { return errors.New("mobile change faild") } go func() { _ = taskser.CompleteOnceTask(nil, uid, oncetaskmod.OnceTaskTypeBindMobile) }() return nil } type CutInfo struct { PromotionCode string `json:"pc"` DC string `json:"dc"` PlatformUserCode string `json:"puc"` Tid string `json:"tid"` } func (this *CutInfo) IsEmpty() bool { return this.PromotionCode == "" && this.DC == "" } // DeviceLogin () 设备登陆 func DeviceLogin(ip string, p usermod.DevLoginP, token string, ua ua.UA) (userInfo usermod.UserRes, code stderr.Code, loginType int, err error) { // iOS 开头的 devType(如 "iOS:25.3.0")统一规范化为 "ios" if strings.HasPrefix(strings.ToLower(p.DevType), "ios") { p.DevType = "ios" } var u *usermod.User var isRegister bool var uid uint64 if token != "" { u, _ = usermod.FindUserByToken(token) if u != nil { uid = u.UID loginType = usermod.TokenLogin } } devID := p.DevID if p.QrCnt != "" { devID, err = GetDevIDFromQrCnt(p.QrCnt) if err != nil { switch err.(type) { case ysqr.ErrContentInvalid: code = stderr.ErrQrInvalid default: code = stderr.ErrNetWorkBusy } return } loginType = usermod.QrCodeLogin } if u == nil { u := &usermod.User{} if devID != "" { u, _ = usermod.FindUserByDevID(devID) } else { code = stderr.ErrDeviceIdAndMobileNil err = errors.New("devID and mobile is nil") return } if u == nil { dc, pc, puc, tid := parseCutInfo(p.CutInfos, ip, p.SysType) ver, err1 := version.New(p.Ver) if err1 == nil && ver.GTE(version.MustNew(constant.Ver3_6_0)) { if !CheckDevIDSign(devID, p.DevToken) { code = stderr.ErrBadDevID err = errors.New("bad args d") log.Info("DeviceLogin Register Bad DevID", log.Any("devLogin", p), log.Any("ip", ip)) return } } // 单 IP 注册总量闸:与 H5Login 共用 register:quota:{ip} 计数,建号前先判定——24h 滚动窗口内 // 同一真实 IP 注册数超额即拒绝并拉黑。放在设备签名校验之后,避免坏 devID 请求白白消耗配额。 // 注意:LoginWithDevice 仅在 err != nil 时才读 code,故这里须与其它拒绝分支一样同时置 err。 if code = checkRegisterQuota(ip); code != stderr.Success { err = errors.New(code.Msg()) return } log.Info("DeviceLogin Register info", log.Any("devLogin", p), log.Any("ip", ip)) uid, token, err = Register(dc, pc, puc, devID, ip, p.SysType, p.DevToken, p.DevType, p.Mobile, p.Ver, constant.TerminalAndroid, ua, tid) if err != nil { return } isRegister = true } else { //切换账号 刷新token token, _ = authuser.GenToken(&authuser.Claims{UID: u.UID, Timestamp: time.Now().UnixNano(), Type: uint8(u.Type)}) uid = u.UID } } if userInfo, err = Info(uid); err != nil { return } //禁止登陆 if userInfo.HasLocked { err = errors.New(stderr.ErrAccessForbid.Msg()) code = stderr.ErrAccessForbid return } if err = lastLogin(uid, token, p.SysType); err != nil { return } userInfo.City = vidhelpser.GetLocationByIP(ip) userInfo.Token = token userInfo.IsRegister = isRegister authuser.RevokeTokenCache(uid) return } func parseCutInfo(cutInfo, ip, sysType string) (dc, pc, puc, tid string) { var _cutInfo CutInfo //获取推广码/商区 _ = json.Unmarshal([]byte(cutInfo), &_cutInfo) log.Info("deviceLogin parseCutInfo info", log.Any("cutInfo", _cutInfo), log.Any("ip", ip), log.Any("systype", sysType)) dc = _cutInfo.DC pc = _cutInfo.PromotionCode puc = _cutInfo.PlatformUserCode tid = _cutInfo.Tid return } func GetDevIDFromQrCnt(qrCnt string) (string, error) { uid, err := ysqr.GetUIDFromQrCnt(ysqr.Login, qrCnt) if err != nil { return "", err } user, err := usermod.FindUserByUID(uid) if err != nil { return "", err } if user == nil { return "", ysqr.ErrContentInvalid{} } c := ysqr.Content{ UID: user.UID, T: ysqr.Login, LoginClaims: ysqr.LoginClaims{ DevID: user.DevID, }, } if qrCnt != c.String(authuser.GetTokenSecret()) { return "", ysqr.ErrContentInvalid{} } return user.DevID, nil } // GetUIDFromQrCnt 校验用户凭证二维码;获取用户uid func GetUIDFromQrCnt(content string) (uid uint64, err error) { uid, err = ysqr.GetUIDFromQrCnt(ysqr.Login, content) if err != nil { return } user, err := usermod.FindUserByUID(uid) if err != nil { return } if user == nil { err = ysqr.ErrInvalidUser return } c := ysqr.Content{ UID: user.UID, T: ysqr.Login, LoginClaims: ysqr.LoginClaims{ DevID: user.DevID, }, } if content != c.String(authuser.GetTokenSecret()) { err = ysqr.ErrContentIllegal return } return uid, nil } func MyRights(uid uint64) (rights usermod.UserRights, err error) { coupons, err := videocoupon.GetCouponsByUID(uid) if err != nil { return } user, err := usermod.FindUserByUID(uid) if err != nil { return } cards, err := getMyCards(uid) if err != nil { return } info, err := Info(uid) if err != nil { return } rights.Cards = cards rights.UID = uid rights.GoldVideoCoupons = MergeGoldVideoCoupons(user, coupons) rights.GoldVideoDiscount = info.PayVidDiscount rights.VipExpireDate = info.VipExpireDate rights.GoldVideoFreeExpire = info.GoldVideoFreeExpire return } func getMyCards(uid uint64) (cards []usermod.Card, err error) { histories, err := prdcthsomod.FindUserProductHistory(uid) if err != nil { return } productIDs := make([]primitive.ObjectID, len(histories)) for i, h := range histories { productIDs[i] = h.ProductID } proucts, err := productmod.FindByProductIDs(productIDs) if err != nil { return } productMap := make(map[primitive.ObjectID]productmod.Product) for _, p := range proucts { productMap[p.ID] = p } now := time.Now() protuctTimeMap := make(map[primitive.ObjectID]time.Duration) for _, h := range histories { if h.ProductType != productmod.VIP { expiration := h.CreatedAt.AddDate(0, 0, productMap[h.ProductID].Duration) if expiration.After(now) { duration, ok := protuctTimeMap[h.ProductID] if !ok { protuctTimeMap[h.ProductID] = expiration.Sub(now) } else { protuctTimeMap[h.ProductID] = duration + expiration.Sub(now) } } } } cards = make([]usermod.Card, len(protuctTimeMap)) i := 0 for id, d := range protuctTimeMap { cards[i] = usermod.Card{ Name: productMap[id].Name, Expiration: now.Add(d), } i++ } return } // Info () 我的信息 func Info(uid uint64) (usermod.UserRes, error) { var u *usermod.User var vT int64 var promoteURL string // 邀请地址 var inviterCode string // 邀请码 var buyVidCount int64 // 用户购买的视频数 var rchgLevel usermod.RechargeLevel // 充值等级 var videoDiscountLog videodiscountmod.VideoDiscountLog // 视频折扣卡 var videoCoupons []videocoupon.UserGoldVideoCoupon // 新版观影券 var whoringCardInfo usermod.WhoringCardInfo // 白嫖卡信息 var payVidDisc int // 视频折扣 var isPopUp bool // 是否弹窗 var totalInvite int64 // 累计邀请人数 u, err := usermod.FindUserByUID(uid) if err != nil || u == nil { return usermod.UserRes{}, err } correctGoldVideoFreeExpire(u) if u.SnapVip { elog, err := exchlogmod.GetLogByUIDAndAuthority(u.UID, exchcodemod.Authority3dPermanentVIP) if err != nil { return usermod.UserRes{}, err } if elog.CreatedAt.Add(time.Hour * 24).Before(time.Now()) { s := 0 u, err = usermod.Update(u.UID, usermod.UserSelector{SnapVip: &s, VipExpireDate: u.OriginVipExpire, VipLevel: u.OriginVipLevel}) if err != nil { return usermod.UserRes{}, err } } } var userInfo usermod.UserRes if u.HasWhoringCard { checkTodayLoginSign(uid) } var isUpgrade bool // 是否vip升级 var wg sync.WaitGroup wg.Add(10) common.Go(func() { defer wg.Done() status := true // 获取会员卡列表 productList, _ := productmod.FindProductsByProductType(&status, false, "") // 根据用户当前卡进行升级 productser.CheckUserUpgradeProducts(uid, productList) for _, v := range productList { if v.IsUpgrade == true { isUpgrade = true return } } }) common.Go(func() { defer wg.Done() userInfo.User = *u if u.VipExpireDate.After(time.Now()) { userInfo.IsVIP = true } else { if u.VipLevel > 0 { _, err = usermod.UpdateUserVipLevel(uid, 0) if err != nil { return } } } userInfo.Age = u.Age() userInfo.Token = "" userInfo.ShowProxy = true }) common.Go(func() { defer wg.Done() ivt, err := proxymod.GetInvrLv1(uid) if err != nil { return } if ivt == 0 { return } u, err := usermod.FindUserByUID(ivt) if err != nil { return } if u != nil { inviterCode = u.PromCode } }) common.Go(func() { defer wg.Done() promoteURL = sourcemod.GetRandomPromotionURL() }) common.Go(func() { defer wg.Done() buyVidCount, _ = countPay4VidLogByUID(uid, "SP") }) common.Go(func() { defer wg.Done() totalInvite, err = proxymod.FindTotal(uid) }) common.Go(func() { defer wg.Done() videoDiscountLog, err = videodiscountmod.GetByUID(uid) if videoDiscountLog.Expiration.After(time.Now()) && u.PayVidDiscount > videoDiscountLog.VideoDiscount { payVidDisc = videoDiscountLog.VideoDiscount } else { payVidDisc = u.PayVidDiscount } }) common.Go(func() { defer wg.Done() videoCoupons, err = videocoupon.GetCouponsByUID(uid) if err != nil { log.Error("Info videocoupon.GetCouponsByUID", log.Any("uid", uid), log.E(err)) } }) //判断用户今日是否领取了金币 common.Go(func() { defer wg.Done() if u.CoinMouthExpireDate.After(time.Now()) { now := time.Now() startTime := timeutil.BeginOfTime(now) endTime := timeutil.BeginningOfTomorrow(now) filter := bson.M{"uid": uid, "createdAt": bson.M{"$gte": startTime, "$lt": endTime}, "desc": bson.M{"$in": []string{"官方充值-购买[金币月卡]", "金币月卡每日赠送金币"}}} opts := options.Find() count, _, _ := txnmod.FindTransactionLogs(filter, opts) if count < 1 { isPopUp = true } } }) common.Go(func() { defer wg.Done() resp := []usermod.DetailInfo{} if u.HasWhoringCard && u.VipExpireDate.After(time.Now()) { // 获取用户的打卡记录信息 whoringCardInfo.HasWhoringCard = u.HasWhoringCard whoringCardInfo.DetailInfo = getSignInfo(uid) } else { whoringCardInfo.DetailInfo = resp } userInfo.WhoringCard = whoringCardInfo }) wg.Wait() userInfo.IsUpgrade = isUpgrade userInfo.IsPopUps = isPopUp userInfo.TotalInvite = totalInvite userInfo.CollectionCount = vT userInfo.Follows = u.Follows userInfo.Fans = u.Fans userInfo.UpTag = u.UpTag userInfo.VipName = u.VipName userInfo.PromoteURL = common.BindUrl(promoteURL, constant.PromotionField+u.PromCode) userInfo.InviterCode = inviterCode userInfo.BuyVidCount = buyVidCount userInfo.RechargeLevel = rchgLevel userInfo.VideoDiscountExpiration = videoDiscountLog.Expiration userInfo.GoldVideoCoupon = Modify(videoCoupons, userInfo.GoldVideoCoupon) userInfo.PayVidDiscount = payVidDisc // 取用户最大视频折扣 userInfo.GoldVideoFreeExpire = u.GoldVideoFreeExpire userInfo.BroadcastExpire = u.BroadcastExpire userInfo.OriVer = u.OriVer userInfo.AdGroup = u.AdGroup // 仅当用户参与广告 AB 测(安卓 + 已分组 + 注册版本号 >= AB 测版本号)时才下发展示类型, // 与广告统计口径 adser.CanRecord 保持一致,避免老版本(注册版本低于门槛)用户误命中。 if adser.CanRecord(u) { configure, _ := sysconfdata.GetAllFromCache() abTestShowTypeMap := configure.GetObject(sysconfmod.VCodeAdverAbTestShowType) if showType, ok := abTestShowTypeMap[string(u.AdGroup)]; ok { userInfo.AdverAbTestShowType, _ = strconv.Atoi(showType) } } userInfo.SendMsgPrice = messageser.CheckChatPrice(u) puc := DLInfo{ 1, u.UID, } tempM := make(map[string]string) tempM["puc"], _ = puc.Enc() b, _ := json.Marshal(tempM) userInfo.AppStoreCode = string(b) //webToken now := time.Now() expiresAt := now.Add(time.Hour * 24 * 7) webToken, _ := authuser.GenWebToken(authuser.WebClaims{ UID: uid, IssuedAt: now.Unix(), ExpiresAt: expiresAt.Unix(), }) userInfo.WebToken = webToken common.Go(func() { _ = taskser.CompleteDailyTask(nil, uid, dailytaskmod.DailyUserLogin) // 连续登陆-成长任务 taskser.CompleteGrowthTask(uid, taskmod.GrowthBuyLogin, userInfo.LoginDays) }) attachImToken(&userInfo) return userInfo, nil } // attachImToken 给用户信息附加 IM SDK 登录凭据 func attachImToken(userInfo *usermod.UserRes) { if userInfo == nil || userInfo.UID == 0 { return } userInfo.ImEnabled = imser.SDKEnabled() if !userInfo.ImEnabled { return } authInfo, err := imser.GetSDKAuth(userInfo.UID) if err != nil { log.Warn("attach im token failed", log.Any("uid", userInfo.UID), log.E(err)) return } userInfo.ImToken = authInfo.ImToken userInfo.ImUserID = authInfo.ImUserID userInfo.ImDynamicDomain = authInfo.DynamicConfigDomain userInfo.ImSocketURL = authInfo.SocketURL } func getSignInfo(uid uint64) (data []usermod.DetailInfo) { resp := []usermod.DetailInfo{} // 获取用户的打卡记录信息 records, err := signrecordmod.GetInfoByUID(uid) if err != nil { return resp } if len(records) > 0 { rts := []usermod.Record{} for i := range records { if len(records[i].RecordTime) > 0 { for _, r := range records[i].RecordTime { rts = append(rts, usermod.Record{ SignTime: r.SignTime, IsSign: r.IsSign, }) } } var price int64 if records[i].ForgetSignDays > 0 { for p := 0; p < int(records[i].ForgetSignDays+records[i].RenewalSignDays); p++ { if p < int(records[i].RenewalSignDays) { continue } price += int64(math.Pow(2, float64(p)) * 10) } } resp = append(resp, usermod.DetailInfo{ ID: records[i].ID, TotalDays: records[i].TotalDays, CurrentSignDays: records[i].CurrentSignDays, RenewalSignDays: records[i].RenewalSignDays, RenewalSignPrice: price, ForgetSignDays: records[i].ForgetSignDays, HasReturn: records[i].HasReturn, RecordTime: rts, StartTime: records[i].CreatedAt, EndTime: records[i].EndTime, }) } } return resp } func checkTodayLoginSign(uid uint64) error { // 获取是否有白嫖卡任务 signRecord, err := signrecordmod.GetInfoByCond(bson.M{"uid": uid}) if err != nil { return err } if signRecord == nil || signRecord.ID.IsZero() { return errors.New("sign record is null") } currentTime := time.Now() if signRecord.CurrentSignDays == signRecord.TotalDays { return nil } totalDays := signRecord.CurrentSignDays + signRecord.ForgetSignDays if totalDays >= signRecord.TotalDays { return nil } // 判断今日是否登录 today := timeutil.BeginningOfDay(time.Now()) if signRecord.SignTime.Before(today) && signRecord.EndTime.After(currentTime) { var isReturn bool var isReturnAmt int64 // 打卡 update := bson.M{"signTime": currentTime, "currentSignDays": signRecord.CurrentSignDays + 1} // 处理是否漏打卡 duration := today.Sub(signRecord.SignTime) days := int64(duration.Hours() / 24) totalForgetSignDays := signRecord.ForgetSignDays + days if days > 0 { // 漏打卡业务 rec := signRecord.RecordTime for i := 1; i <= int(days); i++ { format := currentTime.AddDate(0, 0, -i).Format("2006-01-02") details := signrecordmod.Record{ SignTime: format, IsSign: false, } rec = append(rec, details) } update["recordTime"] = rec update["forgetSignDays"] = totalForgetSignDays } if totalForgetSignDays+signRecord.CurrentSignDays+1 > signRecord.TotalDays { return nil } if signRecord.CurrentSignDays+1 == signRecord.TotalDays { isReturn = true update["hasReturn"] = isReturn // 发放奖励 product, err := productmod.FindOne(nil, bson.M{"_id": signRecord.PID}, options.FindOne()) if err != nil { return err } if product == nil || product.ID.IsZero() { return errors.New("product is null") } isReturnAmt = *product.DiscountedPriceIos } if err = appg.VideoDB.Trans(func(tool *db.MongoTool) error { _, err := signrecordmod.UpdateByCond(tool, bson.M{"_id": signRecord.ID}, update) if err != nil { return err } // 补签打卡扣除金币 var txnLogs []txnmod.TransactionLog if isReturn { wallAct, err := walletmod.CreditAmount(tool, isReturnAmt, uid) if err != nil { return err } tl := txnmod.TransactionLog{ TransNo: primitive.NewObjectID(), UID: uid, Amount: isReturnAmt, ActualAmount: float64(isReturnAmt), TranType: txnmod.SuccessSignReturnAmount.Key(), TranTypeInt: int64(txnmod.SuccessSignReturnAmount), Desc: fmt.Sprintf("%v:%v个", txnmod.SuccessSignReturnAmount.Key(), isReturnAmt), RealAmount: wallAct.RealAmount(), } txnLogs = append(txnLogs, tl) } if len(txnLogs) > 0 { err := txnmod.InsertManyTransactionLog(tool, txnLogs) if err != nil { return err } } return nil }); err != nil { log.Error(fmt.Sprintf("uid:%v, login sign Trans err:%v", uid, err)) return err } } // 如果签到过期 if signRecord.EndTime.Before(currentTime) && totalDays < signRecord.TotalDays { // 打卡 update := bson.M{"signTime": signRecord.EndTime} // 处理是否漏打卡 days := signRecord.TotalDays - signRecord.ForgetSignDays - signRecord.CurrentSignDays totalForgetSignDays := signRecord.ForgetSignDays + days if days > 0 { // 漏打卡业务 rec := signRecord.RecordTime for i := 0; i < int(days); i++ { format := signRecord.EndTime.AddDate(0, 0, -i).Format("2006-01-02") details := signrecordmod.Record{ SignTime: format, IsSign: false, } rec = append(rec, details) } update["recordTime"] = rec update["forgetSignDays"] = totalForgetSignDays } if totalForgetSignDays+signRecord.CurrentSignDays > signRecord.TotalDays { return nil } _, err := signrecordmod.UpdateByCond(nil, bson.M{"_id": signRecord.ID}, update) if err != nil { return err } } return nil } func correctGoldVideoFreeExpire(user *usermod.User) { if user.VideoFreeExpiration != nil && user.VideoFreeExpiration.After(user.GoldVideoFreeExpire) { user.GoldVideoFreeExpire = *user.VideoFreeExpiration } } // Modify 为非H5端修正数据,合并新版数据 func Modify(videoCoupons []videocoupon.UserGoldVideoCoupon, oldCoupons []usermod.UserGoldVideoCoupon) []usermod.UserGoldVideoCoupon { for _, new := range videoCoupons { if !new.Used { isNewNum := true for i, old := range oldCoupons { if old.Gold == new.Num { oldCoupons[i].Count++ isNewNum = false break } } if isNewNum { oldCoupons = append(oldCoupons, usermod.UserGoldVideoCoupon{ Gold: new.Num, Count: 1, }) } } } return oldCoupons } // MergeGoldVideoCoupons 合并新旧版观影券,方便前端展示 func MergeGoldVideoCoupons(u *usermod.User, coupons []videocoupon.UserGoldVideoCoupon) []usermod.GoldVideoCoupon { unusedCouponDetails := []usermod.GoldVideoCouponDetail{} for _, v := range u.GoldVideoCoupon { // 旧版数据均为未使用的 unusedCouponDetails = append(unusedCouponDetails, usermod.GoldVideoCouponDetail{ Num: v.Gold, Count: v.Count, Source: videocoupon.GoldVideoCouponSourceVIP, }) } unusedCoupons := usermod.GoldVideoCoupon{ Position: usermod.CouponPositionUnUsed, Coupons: unusedCouponDetails, } usedCoupons := usermod.GoldVideoCoupon{Position: usermod.CouponPositionUsed, Coupons: []usermod.GoldVideoCouponDetail{}} for _, v := range coupons { // 新版观影券 if !v.Used { // 观影券尚未使用 isNew := true for i, coupon := range unusedCoupons.Coupons { if v.Num == coupon.Num { unusedCoupons.Coupons[i].Count++ isNew = false break } } if isNew { unusedCoupons.Coupons = append(unusedCoupons.Coupons, usermod.GoldVideoCouponDetail{ Num: v.Num, Count: 1, Source: v.Source, }) } } else { // 观影券已被使用 isNew := true for i, coupon := range usedCoupons.Coupons { if v.Num == coupon.Num { usedCoupons.Coupons[i].Count++ isNew = false break } } if isNew { usedCoupons.Coupons = append(usedCoupons.Coupons, usermod.GoldVideoCouponDetail{ Num: v.Num, Count: 1, Source: v.Source, }) } } } ret := []usermod.GoldVideoCoupon{unusedCoupons, usedCoupons} return ret } // OtherInfo () 他的信息 func OtherInfo(iID uint64, hID uint64) (userInfo usermod.UserRes, err error) { var u *usermod.User //var vT int64 var buyVidCount int64 //用户购买的视频数 var rchgLevel usermod.RechargeLevel //充值等级 var videoDiscountLog videodiscountmod.VideoDiscountLog //视频折扣卡 var isFollow bool u, err = usermod.FindUserByUID(hID) if u == nil || err != nil { return } if u.SnapVip { var elog exchlogmod.ExchangeLog elog, err = exchlogmod.GetLogByUIDAndAuthority(u.UID, exchcodemod.Authority3dPermanentVIP) if err != nil { return } if elog.CreatedAt.Add(time.Hour * 24).Before(time.Now()) { s := 0 u, err = usermod.Update(u.UID, usermod.UserSelector{SnapVip: &s, VipExpireDate: u.OriginVipExpire, VipLevel: u.OriginVipLevel}) if err != nil { return } } } var wg sync.WaitGroup wg.Add(5) common.Go(func() { defer wg.Done() isFollow, _ = followmod.IsFollow(iID, hID) }) common.Go(func() { defer wg.Done() userInfo.User = *u if u.VipExpireDate.After(time.Now()) { userInfo.IsVIP = true } else { if u.VipLevel > 0 { _, err = usermod.UpdateUserVipLevel(hID, 0) if err != nil { return } } } userInfo.Age = u.Age() userInfo.Token = "" }) //common.Go(func() { // defer wg.Done() // vT, _ = vidmod.GetPublishedVideoCntByPublisher(hID) //}) common.Go(func() { defer wg.Done() buyVidCount, _ = countPay4VidLogByUID(hID, "SearchSP") }) common.Go(func() { defer wg.Done() rchgLevel, _ = walletser.GetRchgLevel(hID) }) common.Go(func() { defer wg.Done() videoDiscountLog, err = videodiscountmod.GetByUID(iID) if err != nil { log.Error("videodiscountmod.GetByUID", log.Any("iID", iID), log.Any("hID", hID), log.E(err)) } }) //common.Go(func() { // defer wg.Done() // userInfo.HappinessPlazaCount, _ = vidmod.GetVideoCntByPublisher(hID, vidmod.COVER) //}) wg.Wait() userInfo.Fans = u.Fans userInfo.Follows = u.Follows userInfo.UpTag = u.UpTag userInfo.CollectionCount = u.TotalWorks userInfo.BuyVidCount = buyVidCount userInfo.RechargeLevel = rchgLevel userInfo.VideoDiscountExpiration = videoDiscountLog.Expiration userInfo.IsFollow = isFollow return } // BuyVidList () 购买视频列表 func BuyVidList(uid, oid uint64, newsType string, pageSize int64, pageNumber int64) (data []*vidmod.VideoInfoResp, total int64, hasNext bool, err error) { if oid == 0 { oid = uid } //var wg sync.WaitGroup //wg.Add(2) //common.Go(func() { //defer wg.Done() var vl []*payvidlgmod.Pay4VidLog vl, hasNext, err = payvidlgmod.FindByUID(oid, newsType, pageNumber, pageSize) if err != nil { return } keyFunc := func(vid string, uid uint64) string { return fmt.Sprintf("%s-%d", vid, uid) } vlCoinsMap := make(map[string]int64) for _, v := range vl { if v != nil { vlCoinsMap[keyFunc(v.VideoID.Hex(), v.UID)] = v.Coins } } ids := make([]primitive.ObjectID, 0) for _, v := range vl { ids = append(ids, v.VideoID) } data = vidhelpser.GetVideoListByIDsNoStatus(uid, ids) if oid == uid { // 仅当用户查看自己的购买列表时才展示购买时的金币数 for i, v := range data { if v != nil { data[i].Coins = vlCoinsMap[keyFunc(v.ID.Hex(), oid)] } } } //}) //common.Go(func() { // defer wg.Done() // total, _ = countPay4VidLogByUID(oid, newsType) //}) //wg.Wait() return } // GetUserLikeReq type GetUserLikeReq struct { commod.Page UID uint64 `json:"uid" form:"uid"` // 查看那个用户,自己不传 LikeType string `json:"likeType" form:"likeType"` // 点赞类型: SP/SHORT/COVER/PIC/SEED_LINK 或 video/image/text/drama } // UserLikesVidList 用户喜欢的作品列表 func UserLikesVidList(uid uint64, req *GetUserLikeReq) (vList any, hasNext bool, err error) { if req.UID == 0 { req.UID = uid } //var wg sync.WaitGroup //wg.Add(2) //common.Go(func() { // defer wg.Done() var likeList []likemod.LikeOIDRes likeList, hasNext, err = likemod.FindLikesByTypeAndUID(req.LikeType, req.UID, int(req.PageNumber), int(req.PageSize)) var vids []primitive.ObjectID for _, v := range likeList { vids = append(vids, v.ObjID) } if len(vids) <= 0 { vList = make([]*vidmod.VideoInfoResp, 0) return } // 区分普通帖子和ACG switch req.LikeType { case constant.AiPlaza: // 获取ai广场帖子 list, err := aiplazamod.GetAll(bson.M{"_id": bson.M{"$in": vids}}, nil) if err != nil { return vList, hasNext, err } // 按照点赞时间排序重新排排序 dataMap := make(map[primitive.ObjectID]aiplazamod.AiPlaza) for _, v := range list { dataMap[v.ID] = v } infos := []aiplazamod.AiPlaza{} for _, v := range likeList { video, ok := dataMap[v.ObjID] if !ok { continue } infos = append(infos, video) } vList = aiplazadata.FormatAppDataList(infos) case constant.LikeTypeSP, constant.LikeTypeShort, constant.LikeTypeCover, constant.LikeTypePic, constant.LikeTypeSEED_LINK: videos, err := vidmod.GetVideoListByIDs(vids) if err != nil { return vList, hasNext, err } //修正用户喜欢的记录 如果此时vList为0 表示用户喜欢的视频 有大量的发生了状态改变,此时需要对记录表进行修正, 删除掉视频状态不为1的喜欢记录 if len(videos) == 0 { common.Go(func() { vidhelpser.FixLikeVideoRecord(uid, vids) }) } // 按照点赞时间排序重新排排序 videoMap := make(map[primitive.ObjectID]*vidmod.VideoModel) for _, v := range videos { videoMap[v.ID] = v } infos := []*vidmod.VideoModel{} for _, v := range likeList { video, ok := videoMap[v.ObjID] if !ok { continue } infos = append(infos, video) } vList = vidhelpser.NewEncodeVideoInfo(uid, infos) case constant.LikeTypeCartoon, constant.LikeTypeComics, constant.LikeTypeText, constant.LikeTypeDrama: _, m, err := mediamod.GetListByIds(vids) if err != nil { return vList, hasNext, err } // 按照点赞时间排序重新排排序 infos := []*mediamod.Media{} for _, like := range likeList { if media, ok := m[like.ObjID]; ok { if req.LikeType == constant.LikeTypeDrama && (media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete) { continue } infos = append(infos, &media) } } if req.LikeType == constant.LikeTypeDrama { vList = mediaser.FillMedias(infos, uid, true) } else { vList = mediaser.FillNotStatusMedias(infos, true) } //case constant.LikeTypeComment: default: return } //}) //common.Go(func() { // defer wg.Done() // total, _ = countLikeVidByUID(uid, constant.LikeTypeSP) //}) //wg.Wait() return vList, hasNext, nil } func countLikeVidByUID(uid uint64, t string) (total int64, err error) { likes, err := likemod.FindLikeIDSByTypeAndUID(t, uid) if err != nil { return } ids := make([]primitive.ObjectID, 0) for _, v := range likes { ids = append(ids, v.ObjID) } if len(ids) < 1 { return } total, err = vidmod.CountPayVidsByUIDS(ids) if err != nil { return } return } func countPay4VidLogByUID(uid uint64, newsType string) (total int64, err error) { payvids, err := payvidlgmod.FindManyPay4VidLogByUID(uid, newsType) if err != nil { return } ids := make([]primitive.ObjectID, 0) for _, v := range payvids { ids = append(ids, v.VideoID) } if len(ids) < 1 { return } total, err = vidmod.CountPayVidsByUIDS(ids) if err != nil { return } return } // IVList () 作品 func IVList(uid uint64, sortType string, pageSize, pageNumber uint64, playTimeType int) (vList []*vidmod.VideoInfoResp, total int64, hasNext bool, err error) { skip := (pageNumber - 1) * pageSize limit := pageSize vModels, total, hasNext, err := vidmod.GetMyWorkList(uid, sortType, skip, limit, playTimeType) vList = vidhelpser.EncodeVideoInfoBlogger(uid, vModels) return } // HisVList () 作品 func HisVList(uid uint64, oid uint64, pageSize, pageNumber uint64, sortType string, playTimeType int) (vList []*vidmod.VideoInfoResp, total int64, hasNext bool, err error) { var ( // vModels []*vidmod.VideoModel skip, limit int64 status = 1 ) skip = int64((pageNumber - 1) * pageSize) limit = int64(pageSize) //获取rediskey redisKey := redisconst.UserCollectionKey(oid, pageNumber, pageSize, playTimeType, sortType) var data struct { Vmodel []*vidmod.VideoModel Total int64 HasNext bool } //尝试从redis读取 if strP, err1 := appg.Redis.Get(redisKey); strP == nil { //不成功从数据库读取 data.Vmodel, data.Total, data.HasNext, err = vidmod.GetHisWorkList(oid, skip, limit, &status, sortType, playTimeType) if err != nil { return } if err1 == nil { go func() { //写入缓存 b, err := json.Marshal(data) if err != nil { return } err = appg.Redis.Set(redisKey, string(b), redisconst.UserCollectionExpire()) if err != nil { fmt.Println(err.Error()) } }() } } else { if err = json.Unmarshal([]byte(*strP), &data); err != nil { return } } total = data.Total hasNext = data.HasNext vList = vidhelpser.NewEncodeVideoInfo(uid, data.Vmodel) return } // FaqList 常见问题列表 func FaqList(cate string, s commod.StdQuery) (data faqmod.FaqListRes, err error) { result, total, err := faqmod.FaqList(cate, true, s) if err != nil { log.Warn("[METHOD]==>FaqList error", log.E(err)) return } data.Total = total data.List = result return } type DLInfo struct { AppID int `json:"appID"` UID uint64 `json:"uid"` } func (this *DLInfo) Dec(data string) error { b, err := base64.StdEncoding.DecodeString(data) if err != nil { return err } return json.Unmarshal(b, this) } func (this *DLInfo) Enc() (string, error) { b, err := json.Marshal(*this) if err != nil { return "", err } resStr := base64.StdEncoding.EncodeToString(b) return resStr, nil } func checkAccountPassword(ctx context.Context, user *usermod.User, account, pasword string) (code stderr.Code) { if account == "" || user.UID != 0 { return stderr.Success } u, err := usermod.FindUserByAccount(account) if err != nil { log.ErrorX(ctx, "find user by account when register failed", log.Any("account", account), log.E(err)) return stderr.ErrDbQueryError } if u == nil { code = stderr.ErrInvalidAccount return } *user = *u return stderr.Success } func checkLoginEmailMobilePassword(ctx context.Context, user *usermod.User, mobile, email, captchaCode, pwd string) ( code stderr.Code) { if (mobile == "" && email == "") || user.UID != 0 { return stderr.Success // if login without both email and mobile then try other ways, so dont't return err } if code = notiser.VerifyCaptchaCode(ctx, mobile, email, captchaCode); code != stderr.Success { return } // if login with phone number, check whether the mobile has been registered and whether pasword matches if mobile != "" { u, err := usermod.FindUserByMobile(mobile) if err != nil { code = stderr.ErrDbQueryError return } if u == nil { code = stderr.ErrMobileInvalid return } *user = *u } return } func H5LoginByQRCode(ctx context.Context, content, ip string) (userInfo usermod.UserRes, code stderr.Code) { uid, err := GetUIDFromQrCnt(content) if err != nil { log.ErrorX(ctx, "get uid from qr code failed", log.Any("content", content), log.E(err)) code = stderr.ErrQrInvalid return } u, err := usermod.FindUserByUID(uid) if err != nil { log.ErrorX(ctx, "find user by uid failed", log.Any("uid", uid), log.E(err)) code = stderr.UserIsNotExists return } if u == nil || u.UID == 0 { log.WarnX(ctx, "user invalid") code = stderr.UserIsNotExists return } //切换账号 刷新token token, err := authuser.GenToken(&authuser.Claims{UID: u.UID, Timestamp: time.Now().UnixNano(), Type: uint8(u.Type)}) if err != nil { code = stderr.ErrInterServerError return } if err = lastLogin(uid, token, constant.SysTypeIOS); err != nil { return } authuser.RevokeTokenCache(uid) if userInfo, err = Info(uid); err != nil { return } userInfo.Token = token code = stderr.Success return } // H5Login h5登陆 func H5Login(ctx context.Context, ip string, p usermod.LoginWithH5, ua ua.UA) (userInfo usermod.UserRes, code stderr.Code, err error) { code = stderr.Success var u = &usermod.User{} var uid uint64 var token string var isRegister bool // check and find user by email of mobile if provided if code = checkLoginEmailMobilePassword(ctx, u, p.Mobile, p.Email, p.Code, p.PassWord); code != stderr.Success { return } // check and find user by account if provided if code = checkAccountPassword(ctx, u, p.Account, p.PassWord); code != stderr.Success { return } // if login without email and mobile, then check device id if u.UID == 0 { user, err := usermod.FindUserByDevID(p.DevID) if err != nil { code = stderr.ErrServerUnavailable return userInfo, code, err } if user != nil { u = user } } // if it's a new user, then check info in clipboard if u.UID == 0 { // 单 IP 注册总量闸:先于建号判定。24h 滚动窗口内同一真实 IP 注册数超额直接拒绝并拉黑, // 堵住"卡着 IPAutoBlock 的 5 分钟速率闸上限、跨窗口累计出成百上千个号"——速率闸过期即清零,管不住总量。 if code = checkRegisterQuota(ip); code != stderr.Success { return } dc, pc, puc, tid := parseCutInfo(p.CutInfos, ip, p.SysType) log.Info("DeviceLogin Register info", log.Any("devLogin", p), log.Any("ip", ip)) devType := "h5" if p.IsAppStore { devType = "ios" } uid, token, err = Register(dc, pc, puc, p.DevID, ip, p.SysType, "", devType, p.Mobile, "", ua.Terminal, ua, tid) if err != nil { return } isRegister = true } else { // user already existed, then refersh its token uid = u.UID //切换账号 刷新token token, _ = authuser.GenToken(&authuser.Claims{UID: u.UID, Timestamp: time.Now().UnixNano(), Type: uint8(u.Type)}) if err = lastLogin(uid, token, p.SysType); err != nil { return } authuser.RevokeTokenCache(uid) } if userInfo, err = Info(uid); err != nil { return } userInfo.Token = token userInfo.IsRegister = isRegister return } // registerQuota* 为"单 IP 注册总量闸"参数:registerQuotaWindow 滚动窗口内,同一真实 IP 最多允许 // registerQuotaLimit 次注册。区别于 IPAutoBlock 的"每 5 分钟速率闸"——后者窗口过期即清零,拦不住 // 卡着速率上限、跨窗口长期累计的刷号;总量闸按较长窗口封顶,直接堵累计。上限偏严,大出口 NAT 若误伤再调。 const ( registerQuotaWindow = 24 * time.Hour registerQuotaLimit = 10 ) // incrRegisterQuotaScript 原子地对总量闸计数 +1,并在计数首次达到 1 时设置窗口 TTL,返回自增后的计数。 // 用 Lua 保证 INCR+EXPIRE 原子(避免"加了没设过期"的永久残留);Eval 会回传 Redis 错误,供调用方 fail-closed。 const incrRegisterQuotaScript = `local c = redis.call('INCR', KEYS[1]) if c == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end return c` // checkRegisterQuota 单 IP 注册"总量闸":registerQuotaWindow(24h)内同一真实 IP 注册数超过 registerQuotaLimit // 即拒绝并拉黑。与 IPAutoBlock(5 分钟速率闸)互补,专堵"几分钟内爆发 / 卡节奏长期累计"这类速率闸拦不住的刷号。 // Redis 异常时 fail-closed(拒绝):注册是低频操作,宁可此刻挡下新注册,也不在 Redis 被打抖时放行刷号—— // appg.Redis.Incr 封装出错会静默返回 0 相当于放行,这里改用 Eval 显式拿 error 兜住。 func checkRegisterQuota(ip string) stderr.Code { if !appg.ShouldEnforceIPRateLimit() { return stderr.Success } if ip == "" || appg.Redis == nil { return stderr.Success } key := fmt.Sprintf("register:quota:%s", ip) res, err := appg.Redis.Eval(incrRegisterQuotaScript, []string{key}, int(registerQuotaWindow/time.Second)) if err != nil { log.Warn("register quota eval failed, fail-closed reject", log.Any("ip", ip), log.E(err)) return stderr.ErrLoginTooFrequently } cnt, _ := res.(int64) if cnt > registerQuotaLimit { blockRegisterIP(ip, "H5注册24h总量超限自动拉黑") return stderr.ErrLoginTooFrequently } return stderr.Success } // checkRegisterFrequencyAndBlock 注册"速率闸":1 分钟内注册超过 10 次自动拉黑 IP,拦瞬时高频爆发。 func checkRegisterFrequencyAndBlock(ip string) { if !appg.ShouldEnforceIPRateLimit() { return } if ip == "" { return } key := fmt.Sprintf("register:freq:%s", ip) cnt := appg.Redis.Incr(key) if cnt == 1 { _, _ = appg.Redis.ExpireKey(key, 1*time.Minute) } if cnt > 10 { blockRegisterIP(ip, "H5注册频率过高自动拉黑") } } // blockRegisterIP 将 ip 加入注册黑名单:落库 + 刷 Redis 黑名单集合,使其后续请求被 ipblock.IPBlock 中间件 // 在入口直接拦截(连 handler 都进不来)。供总量闸/频率闸命中时复用。 func blockRegisterIP(ip, remark string) { if ip == "" || appg.Redis == nil { return } if err := ipblockmod.Insert(&ipblockmod.IPBlock{ IP: ip, Type: ipblockmod.BlockAct(constant.Register), Remark: remark, }); err != nil { log.Error("insert register ip block failed", log.Any("ip", ip), log.E(err)) } // 刷新IP黑名单缓存 blockKey := redisconst.IPBlockKey(constant.Register) _, _ = appg.Redis.SAdd(blockKey, []interface{}{ip}) log.Info("auto block register ip", log.Any("ip", ip), log.Any("remark", remark)) } // ResetPassword 手机重置密码 func ResetPassword(ctx context.Context, encrypted, password string) stderr.Code { c, err := base64.StdEncoding.DecodeString(encrypted) if err != nil { return stderr.Failure } baseBytes, err := crypt.AesDecrypt(string(c), appg.Conf.Kefu.AppSecret) if err != nil { return stderr.Failure } req := usermod.ResetPasswordRequest{} if err = json.Unmarshal([]byte(baseBytes), &req); err != nil || req.Mobile == "" { return stderr.Failure } return resetPasswordByMobile(ctx, req, password) } func resetPasswordByMobile(ctx context.Context, req usermod.ResetPasswordRequest, password string) stderr.Code { //手机号是否绑定 user, err := usermod.FindUserByMobile(req.Mobile) if err != nil { return stderr.Failure } if user == nil || (user.UID != req.UID) { return stderr.UserIsNotExists } u, err := usermod.Update(user.UID, usermod.UserSelector{PassWord: &password}) if err != nil || u == nil { log.ErrorX(ctx, "reset pwd by mobile failed", log.Any("req", req), log.Any("pwd", password), log.E(err)) return stderr.ErrNetWorkBusy } return stderr.Success } // GenResetWorld 生成验证结果字符串 func GenResetWorld(ctx context.Context, mobile, email string) (stderr.Code, string) { var user *usermod.User if mobile != "" { //手机号是否绑定 user, err := usermod.FindUserByMobile(mobile) if err != nil { return stderr.Failure, "" } if user == nil { return stderr.UserIsNotExists, "" } } mrp := usermod.ResetPasswordRequest{ Mobile: mobile, Email: email, UID: user.UID, TimeStamp: time.Now().Unix(), RandNum: rand.Int63n(9999), } baseBytes, _ := json.Marshal(mrp) sign, err := crypt.AesEncrypt(string(baseBytes), appg.Conf.Kefu.AppSecret) if err != nil { return stderr.Failure, "" } return stderr.Success, base64.StdEncoding.EncodeToString(sign) } // MobileRegister 手机注册 func MobileRegister(uid uint64, mobile, passWord, sys string) (*usermod.UserRes, stderr.Code) { //手机号是否绑定 user, err := usermod.FindUserByMobile(mobile) if err != nil { return nil, stderr.Failure } if user != nil { if user.UID == uid { return nil, stderr.ErrMobileHasBind } return nil, stderr.ErrMobileHasBindByOther } user, err = usermod.FindUserByUID(uid) if err != nil { return nil, stderr.Failure } if user.MobileBindAt != nil { return nil, stderr.ErrInvalidRequest } if err = updateUserMobile(uid, mobile, passWord); err != nil { log.Warn("updateUserMobile error", log.E(err)) return nil, stderr.ErrNetWorkBusy } return refreshToken(uid, sys, uint8(user.Type)) } // RegisterH5 H5注册.手机号、邮箱、账号 func RegisterH5(ctx context.Context, uid uint64, sys string, req RegisterRequestH5) (*usermod.UserRes, stderr.Code) { // if code := notiser.VerifyCaptchaCode(ctx, req.Mobile, req.Email, req.Code); code != stderr.Success { // return nil, code // } // if req.Mobile = strings.TrimSpace(req.Mobile); req.Mobile != "" { // return MobileRegister(uid, req.Mobile, "", sys) // } if req.Account = strings.TrimSpace(req.Account); req.Account != "" { return accountRegister(ctx, uid, req.Account, "", sys) } return nil, stderr.ErrInvalidRequest } func accountRegister(ctx context.Context, uid uint64, account, password, sys string) (*usermod.UserRes, stderr.Code) { // if n, err := filterser.CheckIllegalWords(account); n > 0 || err != nil { // if err != nil { // log.ErrorX(ctx, "check illegal account failed", log.Any("account", account), log.E(err)) // } // return nil, stderr.ErrAccountHasIllegalContent // } user, err := usermod.FindUserByAccount(account) if err != nil { return nil, stderr.Failure } if user != nil { if user.UID == uid { return nil, stderr.ErrAccountHasBinded } return nil, stderr.ErrAccountBindedByOther } user, err = usermod.FindUserByUID(uid) if err != nil { return nil, stderr.Failure } if user.AccountBindAt != nil { return nil, stderr.ErrAccountHasBinded } newUid, err := idmod.GetNextUID() //获取新UID if err != nil { return nil, stderr.ErrNetWorkBusy } if err = updateUserAccount(uid, newUid, account, password); err != nil { log.ErrorX(ctx, "accountRegister failed", log.Any("uid", uid), log.Any("account", account), log.Any("passWord", password), log.E(err)) return nil, stderr.ErrNetWorkBusy } return refreshToken(newUid, sys, uint8(user.Type)) } func refreshToken(uid uint64, sys string, userType uint8) (*usermod.UserRes, stderr.Code) { //切换账号 刷新token token, _ := authuser.GenToken(&authuser.Claims{UID: uid, Timestamp: time.Now().UnixNano(), Type: uint8(userType)}) if err := lastLogin(uid, token, sys); err != nil { return nil, stderr.ErrNetWorkBusy } common.Go(func() { authuser.RevokeTokenCache(uid) }) u, err := Info(uid) if err != nil { return nil, stderr.Success } u.Token = token return &u, stderr.Success } // HandleUserAwardsExpire 处理用户奖章是否过期 func HandleUserAwardsExpire(u *usermod.User) (awardsExpire []usermod.AwardsExpireResp) { var numberIds []int for _, i := range u.AwardsExpire { awardExpire := usermod.AwardsExpireResp{ Number: i.Number, IsExpire: false, } if i.AwardExpire.After(time.Now()) { awardExpire.IsExpire = true } if !sli.ContainsInt(numberIds, i.Number) { numberIds = append(numberIds, i.Number) awardsExpire = append(awardsExpire, awardExpire) continue } if len(awardsExpire) > 0 { for k, v := range awardsExpire { if v.Number == i.Number && awardExpire.IsExpire { awardsExpire[k].IsExpire = awardExpire.IsExpire } } } } return awardsExpire } type ( PublishWorkReq struct { commod.Page Uid *uint64 `form:"uid" binding:"-"` // (获取他人的)用户ID,存在即获取这个用户的作品 NewsType *string `form:"newsType" binding:"-"` // (获取他人的)帖子类型 Status *int `form:"status" binding:"omitempty,oneof=0 1 2"` // (获取自己的)状态: 0:待审核 1:已发布 2:未通过 SortType *int `form:"sortType" binding:"omitempty,oneof=0 1 2"` // 1-最新 2-最多点赞 } PublishWorkRep struct { List []*vidmod.VideoInfoResp `json:"list"` // 视频列表 HasNext bool `json:"hasNext"` // 是否还有下一页 } ) // GetUserPublishWorkList 获取用户发布的作品 func (req *PublishWorkReq) GetUserPublishWorkList(uid uint64) (rep PublishWorkRep, err error) { var ( cond bson.M sort bson.D opt = options.Find().SetSkip(req.Skip64()).SetLimit(req.Limit64()).SetProjection(bson.M{"richText": 0}) ) if req.Uid != nil && *req.Uid > 0 { // 查询他人 if req.NewsType == nil { nt := vidmod.SP req.NewsType = &nt } cond = bson.M{ "publisherID": *req.Uid, "newsType": req.NewsType, "status": vidmod.CheckPass, } sort = bson.D{{Key: "reviewAt", Value: -1}} } else { // 查询自己 if req.Status == nil { status := vidmod.CheckPass req.Status = &status } cond = bson.M{ "publisherID": uid, "newsType": bson.M{"$in": []string{vidmod.SP, vidmod.SHORT, vidmod.PIC, vidmod.COVER}}, "status": req.Status, } sort = bson.D{{Key: "_id", Value: -1}} } if req.SortType != nil && *req.SortType == 1 { sort = bson.D{{Key: "reviewAt", Value: -1}} } else if req.SortType != nil && *req.SortType == 2 { sort = bson.D{{Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}} } vModels, hasNext, err := vidmod.FindList(cond, opt.SetSort(sort)) vList := vidhelpser.EncodeVideoInfoBlogger(uid, vModels) rep.List = vList rep.HasNext = hasNext return rep, err } type ( PublishNumReq struct { } PublishNumRep struct { PassNUm int64 `json:"passNUm"` // 审核通过的数量 PendingReview int64 `json:"pendingReview"` // 待审核的数量 } ) // GetUserPublishNum 获取用户发布的作品数量 func (req *PublishNumReq) GetUserPublishNum(uid uint64) (rep PublishNumRep, err error) { cond := bson.M{ "publisherID": uid, "newsType": bson.M{"$in": []string{vidmod.SP, vidmod.SHORT, vidmod.PIC, vidmod.COVER}}, "status": vidmod.WaitingCheck, } rep.PendingReview, err = vidmod.CountNum(cond) if err != nil { return } cond = bson.M{ "publisherID": uid, "newsType": bson.M{"$in": []string{vidmod.SP, vidmod.SHORT, vidmod.PIC, vidmod.COVER}}, "status": vidmod.CheckPass, } rep.PassNUm, err = vidmod.CountNum(cond) if err != nil { return } return rep, err } type PublishWorkDeleteReq struct { IDs []primitive.ObjectID `form:"ids"` //0:待审核 1:已发布 2:未通过 } // UserPublishWorkDelete 删除我发布但未通过审核的作品[逻辑删除] func (req *PublishWorkDeleteReq) UserPublishWorkDelete(uid uint64) (err error) { cond := bson.M{ "_id": bson.M{"$in": req.IDs}, "publisherID": uid, } updater := bson.M{ "$set": bson.M{"status": vidmod.IsDeleted}, } err = vidmod.UpdateMany(cond, updater) return err }