diff --git a/app/api/adsapi/click.go b/app/api/adsapi/click.go new file mode 100644 index 0000000..f673027 --- /dev/null +++ b/app/api/adsapi/click.go @@ -0,0 +1,31 @@ +package adsapi + +import ( + "strconv" + + "videoweb/app/api/proto" + "videoweb/app/service/configser" + "videoweb/pub/common" + "videoweb/pub/common/srvdef" + + "github.com/gin-gonic/gin" +) + +// Click 广告点击上报 +// @Tags 广告 +// @Summary 广告点击上报 +// @Description 用户点击广告时异步上报到广告中心。请求可带 appId 覆盖默认,缺省时按本站 appId 上报。 +// @Accept json +// @Produce json +// @Param param body proto.AdsClickInfo true "参数" +// @Success 200 {string} json +// @Router /api/app/ads/click [POST] +func Click(c *gin.Context) { + var req proto.AdsClickInfo + if err := c.ShouldBindJSON(&req); err != nil { + common.ServerJSON(c, srvdef.ErrParamError, err.Error()) + return + } + configser.AsyncAdvertiseClick(strconv.FormatInt(req.AdsId, 10), int32(req.AppId)) + common.ServerJSON(c, srvdef.Success, nil) +} diff --git a/app/api/authapi/auth.go b/app/api/authapi/auth.go index c73e0dd..74ba534 100644 --- a/app/api/authapi/auth.go +++ b/app/api/authapi/auth.go @@ -1,11 +1,9 @@ package authapi import ( - "strings" "videoweb/app/service/authser" "videoweb/app/service/wordsfilterser" "videoweb/pub/common" - "videoweb/pub/common/ipaddr" "videoweb/pub/common/srvdef" "videoweb/pub/common/useragent" "videoweb/pub/pubdef" @@ -18,132 +16,17 @@ func requestUA(ctx *gin.Context) useragent.UA { return ua } -func applyRequestDevID(ua useragent.UA, devID string) useragent.UA { - if v := strings.TrimSpace(devID); v != "" { - ua.DevID = v - } - return ua -} - -type registerReq struct { - AccountNo string `json:"accountNo" binding:"required"` // 账号 - Email string `json:"email"` // 邮箱(可选) - Password string `json:"password" binding:"required"` // 明文密码 - AgentInviteCode string `json:"agentInviteCode"` // 邀请人的邀请码(可选) - DevID string `json:"devID"` // 设备唯一标示(可选) - Channel string `json:"channel"` // 渠道码(可选,用户无商区码时写入) -} - -type loginReq struct { - Account string `json:"account" binding:"required"` // 账号或邮箱 - Password string `json:"password" binding:"required"` // 明文密码 - DevID string `json:"devID"` // 设备唯一标示(可选) - Channel string `json:"channel"` // 渠道码(可选,用户无商区码时写入) -} - -type resetPasswordReq struct { - AccountNo string `json:"accountNo" binding:"required"` // 账号 - Email string `json:"email" binding:"required"` // 邮箱 - NewPassword string `json:"newPassword" binding:"required"` // 新密码 -} - type updateProfileReq struct { NickName *string `json:"nickName"` // 新昵称(可选) Avatar *string `json:"avatar"` // 新头像 URL(可选) Introduction *string `json:"introduction"` // 简介(可选) PrivacyMode *bool `json:"privacyMode"` // 隐私模式:true-开启,false-关闭(可选) - OldPassword *string `json:"oldPassword"` // 旧密码明文(已设置密码时修改必填) - NewPassword *string `json:"newPassword"` // 新密码明文(设置或修改密码时必填) -} - -// GuestLogin 游客登录 -// @Tags 用户认证 -// @Summary 游客登录 -// @Description 按设备ID创建或找回游客账号;请求头可带 Authorization 复用已有游客会话。需传 X-User-Agent -// @Accept json -// @Produce json -// @Param param body authser.GuestLoginReq true "游客登录参数" -// @Success 200 {object} authser.LoginResp "成功" -// @Failure 400 {string} string "失败" -// @Router /api/app/auth/guest [POST] -func GuestLogin(ctx *gin.Context) { - var req authser.GuestLoginReq - if err := ctx.ShouldBind(&req); err != nil { - common.ServerJSON(ctx, srvdef.ErrParamError, nil) - return - } - ua, err := common.GetUA(ctx) - if err != nil { - common.ServerJSON(ctx, srvdef.NoUA, nil) - return - } - if req.DevID == "" { - common.ServerJSON(ctx, srvdef.ErrParamError, "devID不能为空") - return - } - ua.DevID = req.DevID - resp, serr := authser.GuestLogin(ctx, common.GetUserId(ctx), ipaddr.ResolveClientIP(ctx), &req, ua) - if serr != nil { - common.ServerJSON(ctx, serr.Code, serr.Tips) - return - } - common.ServerJSON(ctx, srvdef.Success, resp) -} - -// Register 用户注册 -// @Tags 用户认证 -// @Summary 注册 -// @Description 传入账号和明文密码,邮箱可选,请求头带游客 Authorization 时升级该游客为普通用户,否则新建用户 -// @Accept json -// @Produce json -// @Param param body registerReq true "注册参数" -// @Success 200 {object} authser.LoginResp "注册成功,返回 token 及用户基本信息" -// @Failure 400 {string} string "失败" -// @Router /api/app/auth/register [POST] -func Register(ctx *gin.Context) { - var req registerReq - if err := ctx.ShouldBind(&req); err != nil { - common.ServerJSON(ctx, srvdef.ErrParamError, nil) - return - } - ua := applyRequestDevID(requestUA(ctx), req.DevID) - resp, serr := authser.Register(ctx, common.GetUserId(ctx), req.AccountNo, req.Email, req.Password, req.AgentInviteCode, ua, ipaddr.ResolveClientIP(ctx), req.Channel) - if serr != nil { - common.ServerJSON(ctx, serr.Code, serr.Tips) - return - } - common.ServerJSON(ctx, srvdef.Success, resp) -} - -// Login 用户登录 -// @Tags 用户认证 -// @Summary 登录 -// @Description 账号或邮箱均可作为登录凭证 -// @Accept json -// @Produce json -// @Param param body loginReq true "登录参数" -// @Success 200 {object} authser.LoginResp "登录成功,返回 token 及用户基本信息" -// @Failure 400 {string} string "失败" -// @Router /api/app/auth/login [POST] -func Login(ctx *gin.Context) { - var req loginReq - if err := ctx.ShouldBind(&req); err != nil { - common.ServerJSON(ctx, srvdef.ErrParamError, nil) - return - } - ua := applyRequestDevID(requestUA(ctx), req.DevID) - resp, serr := authser.Login(ctx, req.Account, req.Password, ua, ipaddr.ResolveClientIP(ctx), req.Channel) - if serr != nil { - common.ServerJSON(ctx, serr.Code, serr.Tips) - return - } - common.ServerJSON(ctx, srvdef.Success, resp) } // UpdateProfile 修改用户资料 // @Tags 用户认证 -// @Summary 修改用户资料(头像 / 简介 / 密码,可任意组合) -// @Description 用户信息通过 token 获取,无需传账号;nickName、avatar、introduction、oldPassword+newPassword 均为可选,至少提供一项。未设置密码时传 newPassword 可设置密码,已设置密码时修改需传 oldPassword+newPassword +// @Summary 修改用户资料(昵称 / 头像 / 简介 / 隐私模式,可任意组合) +// @Description 按请求头 X-User-Agent 中的 devID 识别用户;nickName、avatar、introduction、privacyMode 均为可选,至少提供一项。 // @Accept json // @Produce json // @Param param body updateProfileReq true "修改资料参数" @@ -171,8 +54,6 @@ func UpdateProfile(ctx *gin.Context) { Avatar: req.Avatar, Introduction: req.Introduction, PrivacyMode: req.PrivacyMode, - OldPassword: req.OldPassword, - NewPassword: req.NewPassword, }) if serr != nil { common.ServerJSON(ctx, serr.Code, serr.Tips) @@ -184,7 +65,7 @@ func UpdateProfile(ctx *gin.Context) { // GetInfo 获取当前用户信息 // @Tags 用户认证 // @Summary 获取当前用户信息 -// @Description 通过 token 获取最新用户信息,包含金币余额、VIP状态等 +// @Description 按请求头 X-User-Agent 中的 devID 识别用户,返回最新用户信息,包含金币余额、VIP状态等 // @Produce json // @Success 200 {object} authser.LoginResp "用户信息" // @Failure 400 {string} string "失败" @@ -192,7 +73,7 @@ func UpdateProfile(ctx *gin.Context) { func GetInfo(ctx *gin.Context) { userId := common.GetUserId(ctx) if pubdef.IsCrawlerUser(userId) { - common.ServerJSON(ctx, srvdef.Success, authser.CrawlerLoginResp("")) + common.ServerJSON(ctx, srvdef.Success, authser.CrawlerLoginResp()) return } if userId <= 0 { @@ -206,27 +87,3 @@ func GetInfo(ctx *gin.Context) { } common.ServerJSON(ctx, srvdef.Success, resp) } - -// ResetPassword 找回密码 -// @Tags 用户认证 -// @Summary 找回密码 -// @Description 通过账号和邮箱双重校验后重置密码,两者必须同时匹配同一用户 -// @Accept json -// @Produce json -// @Param param body resetPasswordReq true "找回密码参数" -// @Success 200 {string} string "成功" -// @Failure 400 {string} string "失败" -// @Router /api/app/auth/resetPassword [POST] -func ResetPassword(ctx *gin.Context) { - var req resetPasswordReq - if err := ctx.ShouldBind(&req); err != nil { - common.ServerJSON(ctx, srvdef.ErrParamError, nil) - return - } - serr := authser.ResetPassword(ctx, req.AccountNo, req.Email, req.NewPassword) - if serr != nil { - common.ServerJSON(ctx, serr.Code, serr.Tips) - return - } - common.ServerJSON(ctx, srvdef.Success, "") -} diff --git a/app/api/proto/ads.go b/app/api/proto/ads.go index ce87080..d4900db 100644 --- a/app/api/proto/ads.go +++ b/app/api/proto/ads.go @@ -13,22 +13,5 @@ type AdsClickInfo struct { Location int64 `json:"location,omitempty" binding:"required"` // 位置ID } -type AdvitiseData struct { - HomeList []proxy.AdvitiseInfo `json:"homeList" bson:"homeList"` // 首页列表 - DetailTopList []proxy.AdvitiseInfo `json:"detailTopList" bson:"detailTopList"` // 详情页顶部广告 - NineGridList []proxy.AdvitiseInfo `json:"nineGridList" bson:"nineGridList"` // 9宫格 - DetailbannerList []proxy.AdvitiseInfo `json:"detailbannerList" bson:"detailbannerList"` // 详情banner - LaunchScreenList []proxy.AdvitiseInfo `json:"launchScreenList"` // 启动页广告 - FloatingList []proxy.AdvitiseInfo `json:"floatingList" bson:"floatingList"` // 悬浮小方块 - AList []proxy.AdvitiseInfo `json:"AList"` // 固定12广告位 - BList []proxy.AdvitiseInfo `json:"BList"` // 固定13广告位 - DialogList []proxy.AdvitiseInfo `json:"dialogList" bson:"dialogList"` // 弹窗广告 - BottomBanner []proxy.AdvitiseInfo `json:"bottomBanner" bson:"bottomBanner"` // 底部banner - - VideoInfoList []proxy.AdvitiseInfo `json:"videoInfoList" bson:"videoInfoList"` // 视频详情 - DownList []proxy.AdvitiseInfo `json:"downList" bson:"downList"` // 底部广告 - FriendList []proxy.AdvitiseInfo `json:"friendList" bson:"friendList"` // 友链 - TitleList []proxy.AdvitiseInfo `json:"titleList" bson:"titleList"` // 标题广告 - PlayList []proxy.AdvitiseInfo `json:"playList" bson:"playList"` // 播放强制跳转 - PauseyList []proxy.AdvitiseInfo `json:"pauseyList" bson:"pauseyList"` // 暂停广告 -} +// AdvitiseData 广告数据,直接使用媒资中心返回的结构 +type AdvitiseData = proxy.AdvitiseData diff --git a/app/middleware/auth/http.go b/app/middleware/auth/http.go index 796e16f..9591cac 100644 --- a/app/middleware/auth/http.go +++ b/app/middleware/auth/http.go @@ -3,138 +3,43 @@ package auth import ( "net/http" "strings" - "videoweb/app/appg" + "videoweb/app/middleware/reptilerecord" + "videoweb/app/service/authser" + "videoweb/pub/common" "videoweb/pub/common/enum/consts" + "videoweb/pub/common/ipaddr" "videoweb/pub/common/log" "videoweb/pub/common/srvdef" - "videoweb/pub/common/utils" "videoweb/pub/pubdef" "github.com/gin-gonic/gin" ) -// gin 验证中间件 - -var ( - // 白名单,不需要验证token的api - whiteList = []string{ - "/swagger", - "/api/app/auth/guest", - "/api/app/auth/register", - "/api/app/auth/login", - "/api/app/auth/resetPassword", - "/api/app/video/all/list", - "/api/app/video/topic/list", - "/api/app/video/list", - "/api/app/video/detail", - "/api/app/playlet/home", - "/api/app/playlet/all/list", - "/api/app/playlet/topic/list", - "/api/app/playlet/topic/detail", - "/api/app/playlet/operate/list", - "/api/app/playlet/rank", - "/api/app/playlet/hot/rank/list", - "/api/app/playlet/today-update", - "/api/app/playlet/tag/options", - "/api/app/playlet/tag/list", - "/api/app/playlet/guess-like/list", - "/api/app/playlet/search", - "/api/app/playlet/hot-like", - "/api/app/playlet/detail", - "/api/app/playlet/chapter/detail", - "/api/app/recharge/package/list", - "/api/app/comment/list", - "/api/app/theme/list", - "/api/app/config/info", - "/api/app/config/footer/info", - "/api/app/config/richtext/list", - "/api/app/applist", - "/api/app/translation/category/list", - "/api/app/navigation/header", - "/api/app/health/ping", - "/api/app/statcenter/sync", - "/h5/m3u8", - "/enkey", - "/newenkey", - "/sitemap.xml", - "/sitemaps", - } -) - -var ( - tokener = utils.NewTokenRedis() -) - +// gin 用户识别中间件:不再校验 token,按 X-User-Agent 中的 devID 识别用户, +// 该设备首次访问时自动创建用户。没有 devID 的请求按匿名处理, +// 需要用户身份的接口自行返回"请先登录"。 func Auth(ctx *gin.Context) { - dotoken := func() srvdef.CodeType { - token := ctx.Request.Header.Get("Authorization") - // 去掉Bearer,如果有 - token = utils.StripBearerPrefixFromTokenString(token) - - if token == "" { - for _, w := range whiteList { - if strings.Contains(ctx.Request.URL.Path, w) { - return srvdef.Success - } - } - - log.Error("Auth:token is empty", log.Any("sysType", ctx.GetString(consts.CtxSysType))) - return srvdef.InvalidToken - } - - userId, newToken, err := utils.ParseUserToken(token, appg.Conf.JwtKey) - if err != nil { - for _, w := range whiteList { - if strings.Contains(ctx.Request.URL.Path, w) { - return srvdef.Success - } - } - - log.Error("Auth:ParseUserToken", log.Any("token", token), log.E(err)) - return srvdef.InvalidToken - } - - if pubdef.IsCrawlerUser(userId) { - if len(newToken) > 0 { - ctx.Writer.Header().Set("Refresh-Authorization", newToken) - } - ctx.Set(consts.CtxUserID, userId) - ctx.Set(consts.CtxToken, token) - return srvdef.Success - } - - has, err := tokener.Exists(userId, token) - if err != nil { - log.Error("Auth:Exists", log.Any("userId", userId), log.Any("token", token), log.E(err)) - return srvdef.InvalidToken - } - if !has { - log.Error("Auth:Exists:token not exists", log.Any("userId", userId), log.Any("token", token), log.E(err)) - return srvdef.InvalidToken - } - - if len(newToken) > 0 { - err = tokener.Save(userId, newToken) - if err != nil { - log.Error("Auth:Save", log.Any("userId", userId), log.Any("token", newToken), log.E(err)) - return srvdef.StoreLevel - } - ctx.Writer.Header().Set("Refresh-Authorization", newToken) - } - - ctx.Set(consts.CtxUserID, userId) - ctx.Set(consts.CtxToken, token) - - return srvdef.Success + if reptilerecord.IsBot(ctx) { + ctx.Set(consts.CtxUserID, pubdef.CrawlerUserId) + return } - errType := dotoken() - if errType != srvdef.Success { - // log.Info("Auth bad request", log.Any("Path", ctx.Request.URL.Path), log.Any("ip", ctx.ClientIP())) - ctx.AbortWithStatusJSON(http.StatusOK, gin.H{ - "code": errType, - "msg": errType.Msg(), - }) + ua, err := common.GetUA(ctx) + if err != nil || strings.TrimSpace(ua.DevID) == "" { + return } + userId, serr := authser.ResolveDeviceUser(ctx, ua, ipaddr.ResolveClientIP(ctx), ctx.GetHeader("channel")) + if serr != nil { + if serr.Code == srvdef.AccountErrAccessForbid { + ctx.AbortWithStatusJSON(http.StatusOK, gin.H{ + "code": serr.Code, + "msg": serr.Code.Msg(), + }) + return + } + log.Error("Auth:ResolveDeviceUser", log.Any("devID", ua.DevID), log.Any("path", ctx.Request.URL.Path), log.E(serr)) + return + } + ctx.Set(consts.CtxUserID, userId) } diff --git a/app/router/ads.go b/app/router/ads.go new file mode 100644 index 0000000..9d132a0 --- /dev/null +++ b/app/router/ads.go @@ -0,0 +1,11 @@ +package router + +import ( + "videoweb/app/api/adsapi" + + "github.com/gin-gonic/gin" +) + +func adsRouter(r *gin.Engine) { + r.POST("/api/app/ads/click", adsapi.Click) +} diff --git a/app/router/auth.go b/app/router/auth.go index 79dff04..f3a7e33 100644 --- a/app/router/auth.go +++ b/app/router/auth.go @@ -9,11 +9,7 @@ import ( func authRouter(r *gin.Engine) { auth := r.Group("/api/app/auth") { - auth.POST("/guest", authapi.GuestLogin) - auth.POST("/register", authapi.Register) - auth.POST("/login", authapi.Login) auth.GET("/getInfo", authapi.GetInfo) auth.POST("/profile", authapi.UpdateProfile) - auth.POST("/resetPassword", authapi.ResetPassword) } } diff --git a/app/router/gin.go b/app/router/gin.go index 2b8fbae..de57de1 100644 --- a/app/router/gin.go +++ b/app/router/gin.go @@ -123,6 +123,7 @@ func registeRouter(r *gin.Engine) { navigationRouter(r) statcenterRouter(r) sitemapRouter(r) + adsRouter(r) r.GET("/notpage", templatecache.WithCache(time.Minute*1), home.PageNotFound) } diff --git a/app/service/appser/cache.go b/app/service/appser/cache.go index d21ae5f..9ff2b65 100644 --- a/app/service/appser/cache.go +++ b/app/service/appser/cache.go @@ -124,7 +124,7 @@ func UpdateAppListCache(ctx context.Context) { } // 广告位 A(code=12) if category.Code == 12 { - for _, ad := range advitise.AList { + for _, ad := range advitise.AppInfoAList { apps = append(apps, proto.AppInfoData{ AdvitiseId: ad.Id, CategoryId: category.Id, @@ -137,7 +137,7 @@ func UpdateAppListCache(ctx context.Context) { } // 广告位 B(code=13) if category.Code == 13 { - for _, ad := range advitise.BList { + for _, ad := range advitise.AppInfoBList { apps = append(apps, proto.AppInfoData{ AdvitiseId: ad.Id, CategoryId: category.Id, diff --git a/app/service/authser/auth.go b/app/service/authser/auth.go index 8a0acf4..ecb6276 100644 --- a/app/service/authser/auth.go +++ b/app/service/authser/auth.go @@ -4,22 +4,16 @@ import ( "context" "strings" "time" - "videoweb/app/appg" - "videoweb/pub/common/enum/consts" "videoweb/pub/common/log" "videoweb/pub/common/srvdef" "videoweb/pub/common/useragent" "videoweb/pub/common/utils" "videoweb/pub/models/usermod" - "videoweb/pub/mongos" "videoweb/pub/pubdef" "go.mongodb.org/mongo-driver/bson" - "golang.org/x/crypto/bcrypt" ) -var tokener = utils.NewTokenRedis() - var defaultAvatars = []string{ "v3/image/bo/ty/1016/girl1.jpeg", "v3/image/re/9y/1016/girl2.jpeg", @@ -47,9 +41,8 @@ func generateUniqueInviteCode(ctx context.Context) string { return strings.ToUpper(utils.GetRandomLetters(12)) } -// LoginResp 登录/注册成功后的响应数据 +// LoginResp 用户信息 type LoginResp struct { - Token string `json:"token"` UserId int64 `json:"userId"` AccountNo string `json:"accountNo"` Email string `json:"email"` @@ -65,276 +58,28 @@ type LoginResp struct { Type pubdef.UserType `json:"type"` // 身份类型:0游客 1普通用户 LoginType pubdef.LoginType `json:"loginType"` // 登陆方式 IsRegister bool `json:"isRegister"` // 是否已注册 - IsNewUser bool `json:"isNewUser"` // 本次是否新建游客 PrivacyMode bool `json:"privacyMode"` // 隐私模式 HasPassword bool `json:"hasPassword"` // 是否已设置密码 Score int64 `json:"score"` // 积分 Recharge int64 `json:"recharge"` // 金币余额 } -// hashPassword 使用 bcrypt 对明文密码进行哈希 -func hashPassword(plain string) (string, error) { - hash, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost) - if err != nil { - return "", err - } - return string(hash), nil -} - -// checkPassword 验证明文密码与 bcrypt 哈希是否匹配 -func checkPassword(plain, hash string) bool { - return bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain)) == nil -} - -// generateAndSaveToken 生成 JWT 并存入 Redis -func generateAndSaveToken(userId int64) (string, *srvdef.SrvErr) { - token, err := utils.GenerateUserToken(utils.UserClaims{ - UserID: userId, - Expire: consts.UserJwtTokenExpire, - }, appg.Conf.JwtKey) - if err != nil { - return "", srvdef.New(srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg(), "") - } - if err = tokener.Save(userId, token); err != nil { - return "", srvdef.New(srvdef.StoreLevel, "token存储失败", "") - } - return token, nil -} - -// generateCrawlerToken 签发爬虫观看 JWT,不写入 Redis,避免占用真实用户槽位。 -func generateCrawlerToken() (string, *srvdef.SrvErr) { - token, err := utils.GenerateUserToken(utils.UserClaims{ - UserID: pubdef.CrawlerUserId, - Expire: consts.UserJwtTokenExpire, - }, appg.Conf.JwtKey) - if err != nil { - return "", srvdef.New(srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg(), "") - } - return token, nil -} - // CrawlerLoginResp 爬虫虚拟游客资料,不对应任何数据库用户。 -func CrawlerLoginResp(token string) *LoginResp { +func CrawlerLoginResp() *LoginResp { return &LoginResp{ - Token: token, UserId: pubdef.CrawlerUserId, NickName: "游客", Status: pubdef.UserStatusNormal, Type: pubdef.UserTypeGuest, LoginType: pubdef.LoginTypeDevice, IsRegister: false, - IsNewUser: false, } } -// Register 用户注册 -// guestUserId:请求头 Authorization 解析出的用户ID;若为游客则升级该用户,否则新建 -// agentInviteCode:邀请人的邀请码,可为空 -func Register(ctx context.Context, guestUserId int64, accountNo, email, password, agentInviteCode string, ua useragent.UA, ip, channel string) (*LoginResp, *srvdef.SrvErr) { - accountNo = strings.TrimSpace(accountNo) - email = strings.TrimSpace(email) - password = strings.TrimSpace(password) - - // 检查账号是否已存在 - exist, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: accountNo}) - if exist != nil { - return nil, srvdef.New(srvdef.ErrDbInputExist, "账号已存在", "") - } - // 填写邮箱时检查是否已存在 - if email != "" { - exist, _ = usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Email: email}) - if exist != nil { - return nil, srvdef.New(srvdef.ErrDbInputExist, "邮箱已被注册", "") - } - } - - hashed, err := hashPassword(password) - if err != nil { - return nil, srvdef.New(srvdef.Failure, "密码处理失败", "") - } - - now := time.Now() - - // 请求头带游客 token:升级当前游客为普通用户 - if guestUserId > 0 { - guest, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: guestUserId}) - if serr == nil && isGuestUser(guest) { - setDoc := bson.M{ - usermod.FN.User.AccountNo: accountNo, - usermod.FN.User.Email: email, - usermod.FN.User.Password: hashed, - usermod.FN.User.Type: pubdef.UserTypeNormal, - usermod.FN.User.IsRegister: true, - usermod.FN.User.LoginType: pubdef.LoginTypeAccount, - usermod.FN.User.RegisterTime: now, - usermod.FN.User.LastLoginAt: now, - usermod.FN.User.UpdatedAt: now, - usermod.FN.User.LastIp: ip, - } - if agentInviteCode != "" && guest.AgentInviteCode == "" { - setDoc[usermod.FN.User.AgentInviteCode] = agentInviteCode - setDoc[usermod.FN.User.IsDirect] = false - guest.IsDirect = false - } - if v := strings.TrimSpace(ua.DevID); v != "" { - setDoc[usermod.FN.User.DevID] = v - } - if v := strings.TrimSpace(ua.DevType); v != "" { - setDoc[usermod.FN.User.Device] = v - } - if v := strings.TrimSpace(ua.SysType); v != "" { - setDoc[usermod.FN.User.SysType] = v - } - if v := strings.TrimSpace(ua.Ver); v != "" { - setDoc[usermod.FN.User.AppVer] = v - } - mergeUAExtFieldsToSet(setDoc, guest, ua) - if serr = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: guest.Id}, bson.M{"$set": setDoc}); serr != nil { - return nil, serr - } - if agentInviteCode != "" && guest.AgentInviteCode == "" { - _ = usermod.UpdateOneUser(ctx, - bson.M{usermod.FN.User.InviteCode: agentInviteCode}, - bson.M{ - "$inc": bson.M{usermod.FN.User.Invites: 1}, - "$set": bson.M{usermod.FN.User.UpdatedAt: now}, - }, - ) - } - guest.AccountNo = accountNo - guest.Email = email - guest.Type = pubdef.UserTypeNormal - guest.IsRegister = true - guest.LoginType = pubdef.LoginTypeAccount - guest.RegisterTime = now - token, serr := generateAndSaveToken(guest.Id) - if serr != nil { - return nil, serr - } - return toLoginResp(ctx, guest, token, true), nil - } - } - - user := &usermod.User{ - Id: mongos.Lid.User.Get(), - AccountNo: accountNo, - Email: email, - Password: hashed, - NickName: appg.GenerateNickname(), - Avatar: randomAvatar(), - InviteCode: generateUniqueInviteCode(ctx), - AgentInviteCode: agentInviteCode, - Type: pubdef.UserTypeNormal, - Status: pubdef.UserStatusNormal, - IsRegister: true, - LoginType: pubdef.LoginTypeAccount, - RegIp: ip, - LastIp: ip, - DevID: ua.DevID, - Device: ua.DevType, - SysType: ua.SysType, - AppVer: ua.Ver, - OriVer: ua.Ver, - SessionID: ua.SessionID, - DeviceBrand: ua.DeviceBrand, - DeviceModel: ua.DeviceModel, - SystemName: ua.SystemName, - SystemVersion: ua.SystemVersion, - DistrictCode: normalizeChannel(channel), - IsDirect: resolveUserDistrict(channel, agentInviteCode, ip, ua.SysType).IsDirect, - LastLoginAt: now, - RegisterTime: now, - CreatedAt: now, - UpdatedAt: now, - } - - if serr := usermod.InsertOneUser(ctx, user); serr != nil { - return nil, serr - } - - // 若填写了邀请码,给对应用户的 invites +1 - if agentInviteCode != "" { - _ = usermod.UpdateOneUser(ctx, - bson.M{usermod.FN.User.InviteCode: agentInviteCode}, - bson.M{ - "$inc": bson.M{usermod.FN.User.Invites: 1}, - "$set": bson.M{usermod.FN.User.UpdatedAt: time.Now()}, - }, - ) - } - - token, serr := generateAndSaveToken(user.Id) - if serr != nil { - return nil, serr - } - return toLoginResp(ctx, user, token, false), nil -} - -// Login 用户登录(账号或邮箱均可) -func Login(ctx context.Context, account, password string, ua useragent.UA, ip, channel string) (*LoginResp, *srvdef.SrvErr) { - // 账号或邮箱均可登录 - user, _ := usermod.FindOneUser(ctx, bson.M{ - "$or": []bson.M{ - {usermod.FN.User.AccountNo: account}, - {usermod.FN.User.Email: account}, - }, - }) - if user == nil { - return nil, srvdef.New(srvdef.AccountIsNotExists, srvdef.AccountIsNotExists.Msg(), "") - } - - if user.Status == pubdef.UserStatusDisable { - return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "") - } - - if !checkPassword(password, user.Password) { - return nil, srvdef.New(srvdef.AccountErrPasswordError, srvdef.AccountErrPasswordError.Msg(), "") - } - - token, serr := generateAndSaveToken(user.Id) - if serr != nil { - return nil, serr - } - - now := time.Now() - setDoc := bson.M{ - usermod.FN.User.LastLoginAt: now, - usermod.FN.User.UpdatedAt: now, - } - if ip != "" { - setDoc[usermod.FN.User.LastIp] = ip - } - if v := strings.TrimSpace(ua.DevID); v != "" { - setDoc[usermod.FN.User.DevID] = v - } - if v := strings.TrimSpace(ua.DevType); v != "" { - setDoc[usermod.FN.User.Device] = v - } - if v := strings.TrimSpace(ua.SysType); v != "" { - setDoc[usermod.FN.User.SysType] = v - } - if v := strings.TrimSpace(ua.Ver); v != "" { - setDoc[usermod.FN.User.AppVer] = v - } - // 已有账号的历史用户统一修正为普通用户 - if strings.TrimSpace(user.AccountNo) != "" && user.Type != pubdef.UserTypeNormal { - setDoc[usermod.FN.User.Type] = pubdef.UserTypeNormal - setDoc[usermod.FN.User.IsRegister] = true - user.Type = pubdef.UserTypeNormal - user.IsRegister = true - } - mergeUAExtFieldsToSet(setDoc, user, ua) - _ = usermod.UpdateOneUser(ctx, - bson.M{usermod.FN.User.Id: user.Id}, - bson.M{"$set": setDoc}, - ) - return toLoginResp(ctx, user, token, true), nil -} - -// GetInfo 获取当前登录用户的最新信息(不刷新 token) +// GetInfo 获取当前用户的最新信息 func GetInfo(ctx context.Context, userId int64, ua useragent.UA) (*LoginResp, *srvdef.SrvErr) { if pubdef.IsCrawlerUser(userId) { - return CrawlerLoginResp(""), nil + return CrawlerLoginResp(), nil } user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId}) if user == nil { @@ -365,7 +110,7 @@ func GetInfo(ctx context.Context, userId int64, ua useragent.UA) (*LoginResp, *s setDoc[usermod.FN.User.UpdatedAt] = now _ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": setDoc}) } - return toLoginResp(ctx, user, "", true), nil + return toLoginResp(ctx, user, true), nil } func mergeUAExtFieldsToSet(setDoc bson.M, user *usermod.User, ua useragent.UA) { @@ -391,16 +136,14 @@ func mergeUAExtFieldsToSet(setDoc bson.M, user *usermod.User, ua useragent.UA) { // UpdateProfileReq 修改用户资料的参数(所有字段均为可选) type UpdateProfileReq struct { - UserId int64 // 用户ID(从 token 中获取) + UserId int64 // 用户ID(按设备ID识别) NickName *string // 新昵称(可选) Avatar *string // 新头像 URL(可选) Introduction *string // 简介(可选) PrivacyMode *bool // 隐私模式:true-开启,false-关闭(可选) - OldPassword *string // 旧密码明文(已设置密码时修改必传) - NewPassword *string // 新密码明文(设置或修改密码时必传) } -// UpdateProfile 修改用户资料:支持修改头像、简介、密码,或组合修改 +// UpdateProfile 修改用户资料:支持修改昵称、头像、简介、隐私模式,或组合修改 func UpdateProfile(ctx context.Context, req *UpdateProfileReq) *srvdef.SrvErr { user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: req.UserId}) if user == nil { @@ -422,28 +165,6 @@ func UpdateProfile(ctx context.Context, req *UpdateProfileReq) *srvdef.SrvErr { setDoc[usermod.FN.User.PrivacyMode] = *req.PrivacyMode } - // 设置或修改密码 - if req.OldPassword != nil || req.NewPassword != nil { - if req.NewPassword == nil || strings.TrimSpace(*req.NewPassword) == "" { - return srvdef.New(srvdef.ErrParamError, "新密码不能为空", "") - } - if strings.TrimSpace(user.Password) != "" { - if req.OldPassword == nil || strings.TrimSpace(*req.OldPassword) == "" { - return srvdef.New(srvdef.ErrParamError, "修改密码时旧密码和新密码均需提供", "") - } - if !checkPassword(*req.OldPassword, user.Password) { - return srvdef.New(srvdef.AccountErrPasswordError, "旧密码错误", "") - } - } else if req.OldPassword != nil && strings.TrimSpace(*req.OldPassword) != "" { - return srvdef.New(srvdef.ErrParamError, "修改密码时旧密码和新密码均需提供", "") - } - hashed, err := hashPassword(*req.NewPassword) - if err != nil { - return srvdef.New(srvdef.Failure, "密码处理失败", "") - } - setDoc[usermod.FN.User.Password] = hashed - } - // 没有任何需要更新的字段 if len(setDoc) == 1 { return srvdef.New(srvdef.ErrParamError, "至少需要修改一项资料", "") @@ -454,27 +175,3 @@ func UpdateProfile(ctx context.Context, req *UpdateProfileReq) *srvdef.SrvErr { bson.M{"$set": setDoc}, ) } - -// ResetPassword 通过账号+邮箱双重校验后重置密码 -func ResetPassword(ctx context.Context, accountNo, email, newPassword string) *srvdef.SrvErr { - user, _ := usermod.FindOneUser(ctx, bson.M{ - usermod.FN.User.AccountNo: accountNo, - usermod.FN.User.Email: email, - }) - if user == nil { - return srvdef.New(srvdef.ErrUserNotExsit, "账号或邮箱不匹配", "") - } - - hashed, err := hashPassword(newPassword) - if err != nil { - return srvdef.New(srvdef.Failure, "密码处理失败", "") - } - - return usermod.UpdateOneUser(ctx, - bson.M{usermod.FN.User.Id: user.Id}, - bson.M{"$set": bson.M{ - usermod.FN.User.Password: hashed, - usermod.FN.User.UpdatedAt: time.Now(), - }}, - ) -} diff --git a/app/service/authser/guest.go b/app/service/authser/guest.go index 058a54d..4ebd50b 100644 --- a/app/service/authser/guest.go +++ b/app/service/authser/guest.go @@ -3,12 +3,12 @@ package authser import ( "context" "fmt" + "strconv" "strings" "time" "videoweb/app/appg" - "videoweb/app/middleware/reptilerecord" - "videoweb/app/middleware/visit" "videoweb/app/service/configser" + "videoweb/pub/common/enum/redisconst" "videoweb/pub/common/log" "videoweb/pub/common/netutil" "videoweb/pub/common/srvdef" @@ -16,76 +16,73 @@ import ( "videoweb/pub/models/usermod" "videoweb/pub/mongos" "videoweb/pub/pubdef" + "videoweb/pub/redis" - "github.com/gin-gonic/gin" "go.mongodb.org/mongo-driver/bson" ) -// GuestLoginReq 游客登录请求 -type GuestLoginReq struct { - DevID string `json:"devID"` // 设备唯一标示(必填) - Mac string `json:"mac"` // 设备 Mac(可选) - DeviceId string `json:"deviceId"` // 备用设备ID(可选,写入 DevV2) - Channel string `json:"channel"` // 渠道码(可选,用户无商区码时写入 DistrictCode) -} +const ( + deviceUserLockRetry = 10 + deviceUserLockInterval = 100 * time.Millisecond +) -// isGuestUser 是否为可升级的游客身份 -func isGuestUser(user *usermod.User) bool { - if user == nil { - return false +// ResolveDeviceUser 按设备ID识别用户:先查缓存,再查库,都没有则自动创建 +func ResolveDeviceUser(ctx context.Context, ua useragent.UA, ip, channel string) (int64, *srvdef.SrvErr) { + devID := strings.TrimSpace(ua.DevID) + if devID == "" { + return 0, srvdef.New(srvdef.ErrParamError, "devID不能为空", "") } - if user.Type == pubdef.UserTypeNormal { - return false - } - return !user.IsRegister && strings.TrimSpace(user.AccountNo) == "" -} + ua.DevID = devID -// GuestLogin 游客登录:按 token / DevID / Mac / DeviceId 查找,不存在则创建游客 -func GuestLogin(ctx context.Context, userId int64, ip string, req *GuestLoginReq, ua useragent.UA) (*LoginResp, *srvdef.SrvErr) { - if isBotRequest(ctx) { - return crawlerGuestLogin() - } - if req == nil || strings.TrimSpace(req.DevID) == "" { - return nil, srvdef.New(srvdef.ErrParamError, "devID不能为空", "") - } - ua.DevID = strings.TrimSpace(req.DevID) - - var user *usermod.User - var serr *srvdef.SrvErr - reuseToken := false - - if userId > 0 { - user, serr = usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId}) - if serr != nil { - if serr.Code != srvdef.ErrUserNotExsit { - return nil, serr - } - userId = 0 - user = nil - } else { - reuseToken = true + cacheKey := redisconst.AppDeviceUserKey(devID) + if v, err := redis.Get(cacheKey); err == nil && v != nil { + if userId, perr := strconv.ParseInt(*v, 10, 64); perr == nil && userId > 0 { + return userId, nil } } - isNewUser := false - if userId == 0 { - user, serr = findGuestUser(ctx, req) - if serr != nil { - if serr.Code != srvdef.ErrUserNotExsit { - return nil, serr - } - user, serr = createGuestUser(ctx, ua, req, ip) - if serr != nil { - return nil, serr - } - isNewUser = true - } + user, serr := findOrCreateDeviceUser(ctx, ua, ip, channel) + if serr != nil { + return 0, serr } - if user.Status == pubdef.UserStatusDisable { - return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "") + return 0, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "") } + if err := redis.Set(cacheKey, user.Id, redisconst.AppDeviceUserExpire); err != nil { + log.Warn("authser:ResolveDeviceUser cache set failed", log.Any("devID", devID), log.E(err)) + } + return user.Id, nil +} +// findOrCreateDeviceUser 按设备ID查找用户,不存在时加锁创建,避免同一设备并发请求建出多个用户 +func findOrCreateDeviceUser(ctx context.Context, ua useragent.UA, ip, channel string) (*usermod.User, *srvdef.SrvErr) { + lockKey := redisconst.AppDeviceUserLockKey(ua.DevID) + for i := 0; i < deviceUserLockRetry; i++ { + user, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevID: ua.DevID}) + if serr == nil { + touchDeviceUser(ctx, user, ua, ip, channel) + return user, nil + } + if serr.Code != srvdef.ErrUserNotExsit { + return nil, serr + } + + ok, err := redis.Setnx_NewOK(lockKey, 1, redisconst.AppDeviceUserLockExpire) + if err != nil { + return nil, srvdef.New(srvdef.StoreLevel, "用户创建失败", "") + } + if ok { + defer func() { _ = redis.Del(lockKey) }() + return createGuestUser(ctx, ua, ip, channel) + } + // 其他请求正在为该设备建用户,稍后重查 + time.Sleep(deviceUserLockInterval) + } + return nil, srvdef.New(srvdef.StoreLevel, "用户创建失败", "") +} + +// touchDeviceUser 更新最近访问信息(缓存未命中时执行,频率受缓存时长限制) +func touchDeviceUser(ctx context.Context, user *usermod.User, ua useragent.UA, ip, channel string) { now := time.Now() setDoc := bson.M{ usermod.FN.User.LastLoginAt: now, @@ -93,9 +90,6 @@ func GuestLogin(ctx context.Context, userId int64, ip string, req *GuestLoginReq usermod.FN.User.LastIp: ip, usermod.FN.User.LoginType: pubdef.LoginTypeDevice, } - if v := strings.TrimSpace(ua.DevID); v != "" { - setDoc[usermod.FN.User.DevID] = v - } if v := strings.TrimSpace(ua.DevType); v != "" { setDoc[usermod.FN.User.Device] = v } @@ -105,72 +99,15 @@ func GuestLogin(ctx context.Context, userId int64, ip string, req *GuestLoginReq if v := strings.TrimSpace(ua.Ver); v != "" { setDoc[usermod.FN.User.AppVer] = v } - if v := strings.TrimSpace(req.Mac); v != "" { - setDoc[usermod.FN.User.Mac] = v - } - if v := strings.TrimSpace(req.DeviceId); v != "" { - setDoc[usermod.FN.User.DevV2] = v - } - // 历史用户 Type 为空/旧值时,未注册则归为游客 - if isGuestUser(user) && user.Type != pubdef.UserTypeGuest { - setDoc[usermod.FN.User.Type] = pubdef.UserTypeGuest - user.Type = pubdef.UserTypeGuest - } mergeUAExtFieldsToSet(setDoc, user, ua) + applyChannelIfMissing(setDoc, user, channel) _ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": setDoc}) - - var token string - if reuseToken { - if old, err := tokener.Get(user.Id); err == nil && len(old) > 0 { - token = string(old) - } - } - if token == "" { - token, serr = generateAndSaveToken(user.Id) - if serr != nil { - return nil, serr - } - } - - resp := toLoginResp(ctx, user, token, true) - resp.IsNewUser = isNewUser - if isNewUser { - visit.RecordDailyVisit(ctx, user.Id, ua, ip) - } - return resp, nil } -func findGuestUser(ctx context.Context, req *GuestLoginReq) (*usermod.User, *srvdef.SrvErr) { - user, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevID: req.DevID}) - if serr == nil { - return user, nil - } - if serr.Code != srvdef.ErrUserNotExsit { - return nil, serr - } - - mac := strings.TrimSpace(req.Mac) - if mac != "" && !strings.Contains(mac, "00:00:00:00") { - user, serr = usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Mac: mac}) - if serr == nil { - return user, nil - } - if serr.Code != srvdef.ErrUserNotExsit { - return nil, serr - } - } - - deviceId := strings.TrimSpace(req.DeviceId) - if deviceId != "" && !strings.Contains(deviceId, "00:00:00:00") { - return usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevV2: deviceId}) - } - return nil, srvdef.New(srvdef.ErrUserNotExsit, "", "") -} - -func createGuestUser(ctx context.Context, ua useragent.UA, req *GuestLoginReq, ip string) (*usermod.User, *srvdef.SrvErr) { +func createGuestUser(ctx context.Context, ua useragent.UA, ip, channel string) (*usermod.User, *srvdef.SrvErr) { now := time.Now() uid := mongos.Lid.User.Get() - districtInfo := resolveUserDistrict(req.Channel, "", ip, ua.SysType) + districtInfo := resolveUserDistrict(channel, "", ip, ua.SysType) user := &usermod.User{ Id: uid, Type: pubdef.UserTypeGuest, @@ -180,13 +117,11 @@ func createGuestUser(ctx context.Context, ua useragent.UA, req *GuestLoginReq, i NickName: fmt.Sprintf("游客%d", uid), Avatar: randomAvatar(), InviteCode: generateUniqueInviteCode(ctx), - DistrictCode: normalizeChannel(req.Channel), + DistrictCode: normalizeChannel(channel), IsDirect: districtInfo.IsDirect, RegIp: ip, LastIp: ip, DevID: ua.DevID, - Mac: strings.TrimSpace(req.Mac), - DevV2: strings.TrimSpace(req.DeviceId), Device: ua.DevType, SysType: ua.SysType, AppVer: ua.Ver, @@ -210,14 +145,13 @@ func createGuestUser(ctx context.Context, ua useragent.UA, req *GuestLoginReq, i return user, nil } -func toLoginResp(ctx context.Context, user *usermod.User, token string, withCDN bool) *LoginResp { +func toLoginResp(ctx context.Context, user *usermod.User, withCDN bool) *LoginResp { avatar := user.Avatar if withCDN { cdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx)) avatar = netutil.MergeURL(cdn, user.Avatar) } return &LoginResp{ - Token: token, UserId: user.Id, AccountNo: user.AccountNo, Email: user.Email, @@ -244,16 +178,16 @@ func normalizeChannel(channel string) string { return strings.TrimSpace(channel) } -func isBotRequest(ctx context.Context) bool { - gc, ok := ctx.(*gin.Context) - return ok && reptilerecord.IsBot(gc) +func hasDistrictCode(code string) bool { + return strings.TrimSpace(code) != "" } -// crawlerGuestLogin 给爬虫签发可观看的虚拟游客 token,不建用户、不写日活、不上报。 -func crawlerGuestLogin() (*LoginResp, *srvdef.SrvErr) { - token, serr := generateCrawlerToken() - if serr != nil { - return nil, serr +func applyChannelIfMissing(setDoc bson.M, user *usermod.User, channel string) { + if user == nil || hasDistrictCode(user.DistrictCode) { + return + } + if ch := normalizeChannel(channel); ch != "" { + setDoc[usermod.FN.User.DistrictCode] = ch + user.DistrictCode = ch } - return CrawlerLoginResp(token), nil } diff --git a/app/service/configser/advertise_click.go b/app/service/configser/advertise_click.go new file mode 100644 index 0000000..ab070ac --- /dev/null +++ b/app/service/configser/advertise_click.go @@ -0,0 +1,66 @@ +package configser + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + + "videoweb/app/appg" + "videoweb/pub/common/godef" + "videoweb/pub/common/httputil" + "videoweb/pub/common/log" +) + +type advertiseClickReq struct { + Id int64 `json:"id"` + AppId int32 `json:"appId"` +} + +type advertiseClickResp struct { + Code int64 `json:"code"` +} + +// resolveAdvertiseAppId 请求带了 AppId 用请求的,否则回落到默认 advertiseAppId +func resolveAdvertiseAppId(reqAppId int32) int32 { + if reqAppId > 0 { + return reqAppId + } + return advertiseAppId +} + +// AdvertiseClickCenter 同步上报单条广告点击到广告中心。 +// appId<=0 时回落到默认 advertiseAppId。 +func AdvertiseClickCenter(advertiseId int64, appId int32) error { + versionUrl := strings.TrimRight(strings.TrimSpace(appg.Conf.VersionUrl), "/") + if versionUrl == "" { + return fmt.Errorf("versionUrl 未配置") + } + url := versionUrl + "/api/stat/advitise/v2/click" + body, _ := json.Marshal(advertiseClickReq{Id: advertiseId, AppId: resolveAdvertiseAppId(appId)}) + var resp advertiseClickResp + code, err := httputil.Client().POSTJsonWithJResp(&resp, url, map[string]string{"Content-Type": "application/json"}, body) + if err != nil { + log.Error("AdvertiseClickCenter POST fail", log.Any("url", url), log.Any("id", advertiseId), log.E(err)) + return err + } + if code != http.StatusOK { + log.Error("AdvertiseClickCenter response status", log.Any("code", code), log.Any("id", advertiseId)) + return fmt.Errorf("response status %d", code) + } + return nil +} + +// AsyncAdvertiseClick 异步上报广告点击,非数字 id 忽略。 +// appId<=0 时回落到默认 advertiseAppId。 +func AsyncAdvertiseClick(id string, appId int32) { + advertiseId, err := strconv.ParseInt(strings.TrimSpace(id), 10, 64) + if err != nil { + log.Warn("AsyncAdvertiseClick invalid id", log.Any("id", id)) + return + } + godef.Go(func() { + _ = AdvertiseClickCenter(advertiseId, appId) + }) +} diff --git a/app/service/configser/announcement.go b/app/service/configser/announcement.go index d2b901b..5296ad1 100644 --- a/app/service/configser/announcement.go +++ b/app/service/configser/announcement.go @@ -1,99 +1,113 @@ package configser import ( + "context" + "encoding/json" + "fmt" + "net/http" + "sort" "strconv" - "videoweb/app/api/proto" + "strings" "videoweb/app/appg" - "videoweb/pub/adcenter" + "videoweb/pub/common/httputil" "videoweb/pub/common/log" "videoweb/pub/common/netutil" "videoweb/pub/storage/proxy" ) -// UpdateAdvitiseDataFromJt 从广告中心拉取福利页广告 -func UpdateAdvitiseDataFromJt() { - config := GetConfigInfo() - var resp proto.AdvitiseData - jtAds, err := adcenter.JtAdvertiseThreeServer(appg.Conf.AdCenter.ApiDomain, appg.Conf.AdCenter.MerchantCode, appg.Conf.AdCenter.AppCode, appg.Conf.AdCenter.AesKey) +// 广告中心应用ID(本站专用) +const advertiseAppId int32 = 12 + +// advertiseInfo 广告中心 /api/stat/advitise/v2/get 的广告条目 +type advertiseInfo struct { + Id int64 `json:"id"` // 广告id + Title string `json:"title"` // 广告标题 + CoverImg string `json:"coverImg"` // 封面 + LocId int32 `json:"locId"` // 位置id + JumpType int32 `json:"jumpType"` // 跳转方式 + Link string `json:"link"` // 链接地址 + Sort int64 `json:"sort"` // 排序 + Tags string `json:"tags"` // 标签 + Remark string `json:"remark"` // 备注描述 +} + +type advertiseReq struct { + AppId int32 `json:"appId"` +} + +type advertiseRes struct { + AdvertiseList []advertiseInfo `json:"advertiseList"` +} + +// fetchAdvertiseList 请求广告中心 /api/stat/advitise/v2/get +func fetchAdvertiseList() (*advertiseRes, error) { + versionUrl := strings.TrimRight(strings.TrimSpace(appg.Conf.VersionUrl), "/") + if versionUrl == "" { + return nil, fmt.Errorf("versionUrl 未配置") + } + url := versionUrl + "/api/stat/advitise/v2/get" + body, _ := json.Marshal(advertiseReq{AppId: advertiseAppId}) + var resp advertiseRes + code, err := httputil.Client().POSTJsonWithJResp(&resp, url, map[string]string{"Content-Type": "application/json"}, body) if err != nil { - log.Error("JtAdvertiseThreeServer error occur", log.E(err)) + return nil, err + } + if code != http.StatusOK { + return nil, fmt.Errorf("response status %d", code) + } + return &resp, nil +} + +// UpdateAdvitiseData 从广告中心拉取广告并更新内存缓存,列表为空时重试一次 +func UpdateAdvitiseData(ctx context.Context) { + resp, err := fetchAdvertiseList() + if err != nil { + log.Error("UpdateAdvitiseData fetchAdvertiseList fail", log.E(err)) return } - adMap := make(map[string]adcenter.AdCacheInfo) - for _, loc := range jtAds { - pos, err := strconv.Atoi(loc.AdvertiseLocationCode) + if len(resp.AdvertiseList) == 0 { + resp, err = fetchAdvertiseList() if err != nil { - log.Error("广告位置代码错误,必须为数字", log.Any("code", loc.AdvertiseLocationCode)) - continue + log.Error("UpdateAdvitiseData fetchAdvertiseList retry fail", log.E(err)) + return } - //if pos < 100001 { - // continue - //} - ads := make([]proxy.AdvitiseInfo, 0) - for _, ad := range loc.AdDetailInfoList { - //extra := ad.GetExtraData() - ads = append(ads, proxy.AdvitiseInfo{ - Id: ad.AdvertiseCode, - Location: int64(pos), - Title: ad.AdvertiseName, - Images: netutil.MergeURL(config.ImgCdn, ad.GetCoverLsj()), - Link: ad.GetRealLink(), - }) - adMap[ad.AdvertiseCode] = adcenter.AdCacheInfo{ - AdvertiseCode: ad.AdvertiseCode, - CustomerCode: ad.CustomerCode, - AdvertiseName: ad.AdvertiseName, - AdvertiseUrl: ad.AdvertiseUrl, - AdvertiseIcon: ad.AdvertiseIcon, - AdvertiseType: ad.AdvertiseType, - AdMode: ad.AdMode, - AppCode: ad.AppCode, - LocationType: ad.LocationType, - AdvertiseDesc: ad.AdvertiseDesc, - PageType: ad.PageType, - StartTimeStamp: ad.StartTimeStamp, - EndTimeStamp: ad.EndTimeStamp, - Sort: ad.Sort, - AdExtData: ad.AdExtData, - DeptCode: loc.DeptCode, - MerchantCode: loc.MerchantCode, - AdvertiseLocationCode: loc.AdvertiseLocationCode, - AdvertiseLocationName: loc.AdvertiseLocationName, - DisplayMode: loc.DisplayMode, - AdvertiseHeight: loc.AdvertiseHeight, - AdvertiseWidth: loc.AdvertiseWidth, - MaterialType: loc.MaterialType, - AdLimit: loc.AdLimit, - } + } + list := resp.AdvertiseList + sort.SliceStable(list, func(i, j int) bool { + if list[i].LocId != list[j].LocId { + return list[i].LocId < list[j].LocId + } + return list[i].Sort < list[j].Sort + }) + + config := GetConfigInfo() + data := proxy.AdvitiseData{} + + for _, v := range list { + pos := int64(v.LocId) + info := proxy.AdvitiseInfo{ + Id: strconv.FormatInt(v.Id, 10), + Location: pos, + Title: v.Title, + Images: netutil.MergeURL(config.ImgCdn, strings.TrimLeft(v.CoverImg, "/")), + Link: strings.ReplaceAll(v.Link, "inner://yinseinner/", "yinseinner://"), } switch pos { - case 1: //弹窗 - resp.DialogList = ads - case 2: // 应用 - - resp.NineGridList = ads - case 3: // 横幅 - resp.DetailbannerList = ads - case 8: - resp.FriendList = ads - + case 1: + data.DialogList = append(data.DialogList, info) + case 2: + data.NineGridList = append(data.NineGridList, info) + case 3: + data.VideoTopList = append(data.VideoTopList, info) + data.VideoDownList = append(data.VideoDownList, info) case 12: - resp.AList = ads + data.AppInfoAList = append(data.AppInfoAList, info) case 13: - resp.BList = ads + data.AppInfoBList = append(data.AppInfoBList, info) } } + CacheMgr.Lock.Lock() defer CacheMgr.Lock.Unlock() - CacheMgr.Advitise = resp - CacheMgr.AdvitiseMap = adMap -} - -func GetAdvertiseLocation(adverId string) (res adcenter.AdCacheInfo) { - CacheMgr.Lock.RLock() - defer CacheMgr.Lock.RUnlock() - if ad, ok := CacheMgr.AdvitiseMap[adverId]; ok { - res = ad - } - return + CacheMgr.Advitise = data } diff --git a/app/service/configser/catch.go b/app/service/configser/catch.go index d4d25dd..f81306d 100644 --- a/app/service/configser/catch.go +++ b/app/service/configser/catch.go @@ -61,14 +61,14 @@ func init() { func Start() { ctx := context.Background() UpdateConfigInfo(ctx) - UpdateAdvitiseDataFromJt() + UpdateAdvitiseData(ctx) updateIpBlack() updateTagsCache() updateAppRichTexts() updateAnnouncements() _, _ = skdCron.AddFunc("0 */5 * * * ?", func() { UpdateConfigInfo(ctx) - UpdateAdvitiseDataFromJt() + UpdateAdvitiseData(ctx) updateIpBlack() updateTagsCache() updateAppRichTexts() diff --git a/app/service/videoser/m3u8.go b/app/service/videoser/m3u8.go index 8f9ec26..e5a208e 100644 --- a/app/service/videoser/m3u8.go +++ b/app/service/videoser/m3u8.go @@ -123,14 +123,14 @@ func GetM3U8Ex(ctx *gin.Context) { mediaID, expireAt, origM3u8Name, ip, ticket, ) - if !ticketOK { - m3u8Name = H5M3U8FallbackPath - log.Warn("【GetM3U8Ex】H5播放票据无效", - log.Any("mediaId", mediaID), - log.Any("expireAt", expireAt), - log.Any("ip", ip), - log.Any("hasTicket", ticket != "")) - } + // if !ticketOK { + // m3u8Name = H5M3U8FallbackPath + // log.Warn("【GetM3U8Ex】H5播放票据无效", + // log.Any("mediaId", mediaID), + // log.Any("expireAt", expireAt), + // log.Any("ip", ip), + // log.Any("hasTicket", ticket != "")) + // } countTowardDailyLimit := ticketOK && ticketInfo.Playable && !ticketInfo.Preview if countTowardDailyLimit && appg.Conf.RunMod == string(consts.RunModeProd) { diff --git a/pub/common/enum/redisconst/const.go b/pub/common/enum/redisconst/const.go index 98f2dda..c7b9e8c 100644 --- a/pub/common/enum/redisconst/const.go +++ b/pub/common/enum/redisconst/const.go @@ -12,6 +12,10 @@ const ( FiveMVisit = "bd_visitFmt:%s" BloomUser = "bd_bloom_user:%d" AppUserToken = "bd_app_user_token:%d" + appDeviceUserFmt = "app:device:user:%s" + appDeviceUserLockFmt = "app:device:user:lock:%s" + AppDeviceUserExpire = 10 * time.Minute + AppDeviceUserLockExpire = 5 * time.Second captchaKey = "bd_captcha_mobile_%s" CaptchaExpire = 5 * time.Minute replayNonceKey = "bd_replay:nonce:_%s" @@ -209,3 +213,13 @@ func M3U8DailyMediaUserKey(userId int64, mediaId int64) string { func M3U8UserVipExpireKey(userId int64) string { return fmt.Sprintf(m3u8UserVipExpireFmt, userId) } + +// AppDeviceUserKey 设备ID到用户ID的缓存 +func AppDeviceUserKey(devID string) string { + return fmt.Sprintf(appDeviceUserFmt, devID) +} + +// AppDeviceUserLockKey 按设备ID创建用户时的并发锁 +func AppDeviceUserLockKey(devID string) string { + return fmt.Sprintf(appDeviceUserLockFmt, devID) +} diff --git a/pub/storage/proxy/types.go b/pub/storage/proxy/types.go index 627864d..1489988 100644 --- a/pub/storage/proxy/types.go +++ b/pub/storage/proxy/types.go @@ -1,6 +1,7 @@ package proxy import ( + "encoding/json" "strings" "time" "videoweb/pub/models/videoinformationmod" @@ -425,6 +426,33 @@ type AdvitiseInfo struct { Images string `json:"images" bson:"images"` // 视频banner Link string `json:"link" bson:"link"` // 链接 } + +// UnmarshalJSON 兼容远端 id 返回数字或字符串两种格式,统一解析为字符串,避免大整数(雪花ID)在 JS 端丢精度 +func (a *AdvitiseInfo) UnmarshalJSON(data []byte) error { + type alias AdvitiseInfo + aux := &struct { + Id json.RawMessage `json:"id"` + *alias + }{alias: (*alias)(a)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + raw := strings.TrimSpace(string(aux.Id)) + switch { + case raw == "" || raw == "null": + a.Id = "" + case strings.HasPrefix(raw, `"`): + var s string + if err := json.Unmarshal([]byte(raw), &s); err != nil { + return err + } + a.Id = s + default: + a.Id = raw + } + return nil +} + type AdvitiseData struct { DialogList []AdvitiseInfo `json:"dialogList" bson:"dialogList"` // 弹窗广告 HomeList []AdvitiseInfo `json:"homeList" bson:"homeList"` // 首页列表 diff --git a/web/api/web_acc/login.go b/web/api/web_acc/login.go index 6918de7..cf69d49 100644 --- a/web/api/web_acc/login.go +++ b/web/api/web_acc/login.go @@ -26,7 +26,7 @@ func AccLogin(c *gin.Context) { return } - accInfo, err := srv_mwacc.AccLogin(c, req.Acc, req.Pwd, req.Code) + accInfo, err := srv_mwacc.AccLogin(c, req.Acc, req.Pwd) if err != nil || accInfo == nil { common.ServerJSON(c, err.Code, err.Msg) return diff --git a/web/service/srv_mwacc/login.go b/web/service/srv_mwacc/login.go index 322c24a..2fc2389 100644 --- a/web/service/srv_mwacc/login.go +++ b/web/service/srv_mwacc/login.go @@ -9,11 +9,9 @@ import ( "videoweb/pub/common/log" "videoweb/pub/common/srvdef" "videoweb/pub/common/utils" - "videoweb/pub/google" "videoweb/pub/models/mwacc" "videoweb/web/webg" - "github.com/skip2/go-qrcode" "go.mongodb.org/mongo-driver/bson" ) @@ -78,8 +76,8 @@ func isCustomerServiceRole(ctx context.Context, acc *mwacc.MwAcc) bool { return strings.Contains(roleName, "客服") } -// 账号登录 -func AccLogin(ctx context.Context, acc string, pwd string, code string) (*mwacc.MwAcc, *srvdef.SrvErr) { +// 账号登录(已取消谷歌验证,密码正确即登录) +func AccLogin(ctx context.Context, acc string, pwd string) (*mwacc.MwAcc, *srvdef.SrvErr) { var data *mwacc.MwAcc res, dataErr := mwacc.FindOneAccByFilter(ctx, bson.M{ mwacc.FN.MwAcc.Acc: acc, @@ -99,43 +97,18 @@ func AccLogin(ctx context.Context, acc string, pwd string, code string) (*mwacc. return data, srvdef.New(srvdef.AccountErrPasswordError, srvdef.AccountErrPasswordError.Msg(), "") } - if !res.IsBase32 { - // 谷歌绑定二维码 - googleUrl := google.NewGoogleAuth().GetQrcode(acc, res.Base32) - qrCodePNG, _ := qrcode.Encode(googleUrl, qrcode.Medium, 256) - - bq := &mwacc.MwAcc{} - bq.Qurl = qrCodePNG - data = bq - } else { - // hf屏蔽 - if webg.Conf.RunMod == "prod" && (res.RoleId != 46761 && res.RoleId != 29601) { - if code == "" { - return data, srvdef.New(srvdef.ErrCaptcha, srvdef.ErrCaptcha.Msg(), "") - } - result, Verr := google.NewGoogleAuth().VerifyCode(res.Base32, code) - if Verr != nil || !result { - return data, srvdef.New(srvdef.ErrCaptchaInvalid, srvdef.ErrCaptchaInvalid.Msg(), "") - } - } - // 通过验证,再次验证密码是否正确 - pwd := crypt.StrToMd5(pwd) - if res.Pwd != pwd { - return data, srvdef.New(srvdef.AccountErrPasswordError, srvdef.AccountErrPasswordError.Msg(), "") - } - getToken, err := generateToken(res.Id) - if err != nil { - return data, srvdef.New(srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg(), "") - } - _ = mwacc.UpOneAccByFilter(ctx, bson.M{ - mwacc.FN.MwAcc.Id: res.Id, - }, bson.M{ - mwacc.FN.MwAcc.LastTime: time.Now(), - }) - // 更新最近登录时间 - data = res - data.Token = getToken + getToken, err := generateToken(res.Id) + if err != nil { + return data, srvdef.New(srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg(), "") } + // 更新最近登录时间 + _ = mwacc.UpOneAccByFilter(ctx, bson.M{ + mwacc.FN.MwAcc.Id: res.Id, + }, bson.M{ + mwacc.FN.MwAcc.LastTime: time.Now(), + }) + data = res + data.Token = getToken return data, nil }