Files
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

2045 lines
59 KiB
Go
Raw Permalink 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 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
}