From 97f679d13024f6d2d3777acd07b28caa3f46bcde Mon Sep 17 00:00:00 2001 From: fish Date: Thu, 8 Oct 2026 17:18:41 +0800 Subject: [PATCH] sync: port tangguo_server upstream changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ported 7 commits (3a79d6a..3dcf693) from tangguo_server: - 版本控制: comment out 2FA in admin.go - 支付和广告调试: pay/ad refactor across appg/adser/rechargeser/pingctrl - 对接统计: add bili_sync statistics endpoint - 代码优化: ginUtil improvements - 修改es/修改appID/代码调整: cumulative updates Preserved hgmj-local differences: - ES table prefix stays 91jd_ (not tangguo_) - BillPlatformID stays 91短剧 (not 糖果短剧) - admin googauth label stays @91短剧 - appg.go retains LouFengH5/DataBuriedPoint fields - rechargeser.go retains reportser event reporting Co-Authored-By: Claude Opus 4.7 --- app/api/adsctrl/ads.go | 1 + app/api/pingctrl/pingctrl.go | 34 +- app/api/pingctrl/version.go | 73 +++++ app/api/rechargectrl/yinse.go | 7 + app/api/recreationctrl/recreationctrl.go | 1 + app/appg/appg.go | 1 + app/service/adser/advertiseV2.go | 2 + app/service/adser/jtAdvertise.go | 222 +++++++------ app/service/rechargeser/new_pay.go | 16 +- app/service/rechargeser/rechargeser.go | 33 +- common/ginUtil.go | 31 +- common/m3u8/m3u8.go | 2 +- common/rchgutil/bill.go | 43 ++- common/rchgutil/recharge.go | 91 +++--- web/api/adminctrl/admin.go | 22 +- web/api/statcenterctl/bili_sync.go | 379 +++++++++++++++++++++++ web/router/base.go | 5 + web/router/sync.go | 5 + 18 files changed, 750 insertions(+), 218 deletions(-) create mode 100644 app/api/pingctrl/version.go create mode 100644 web/api/statcenterctl/bili_sync.go diff --git a/app/api/adsctrl/ads.go b/app/api/adsctrl/ads.go index 75f1288..b5c48d4 100644 --- a/app/api/adsctrl/ads.go +++ b/app/api/adsctrl/ads.go @@ -37,6 +37,7 @@ func AdsClick(ctx *gin.Context) { common.ServeJSON(ctx, stderr.ErrParamError, nil) return } + adser.AsyncAdvertiseClick(arg.ID) common.ServeJSON(ctx, stderr.Success, nil) } diff --git a/app/api/pingctrl/pingctrl.go b/app/api/pingctrl/pingctrl.go index 1bce7af..c9d2178 100644 --- a/app/api/pingctrl/pingctrl.go +++ b/app/api/pingctrl/pingctrl.go @@ -12,6 +12,7 @@ import ( "91jd-server/common/constant" "91jd-server/common/services/message" "91jd-server/common/store" + v10 "91jd-server/common/v10" "91jd-server/models/cache/bannerjumpdata" "91jd-server/models/cache/sysconfdata" "91jd-server/models/v/bannerjumpmod" @@ -33,7 +34,6 @@ import ( "91jd-server/common" "91jd-server/common/log" "91jd-server/common/stderr" - "91jd-server/common/version" "91jd-server/models/commod" "91jd-server/models/v/sourcemod" "91jd-server/models/v/systemmod" @@ -249,40 +249,30 @@ func DomainList(ctx *gin.Context) { }) common.Go(func() { defer wg.Done() + builId := v10.ExtractPureEngChar(ua.BuildID) //版本、广告、公告 - verResp, _, annouResp, _, _, _, err := versionser.AdvVersionAnnounThreeServer(ua.Ver, ua.SysType) + versionResp, err := VersionThreeServer(ua.Ver, ua.SysType, builId, ua.BuildID) if err != nil { log.Error("VersionThreeServer", log.E(err)) + return } - for k, v := range annouResp { - v.Href = activityclient.ReplaceActivityDomain(v.Href, user, wallet) - annouResp[k] = v - } - //版本业务 - //安卓不做限制;iOS 端版本 <= 1.11.2 不下发版本信息 - skipVer := false - if ua.SysType == constant.SysTypeIOS { - if cur, err := version.New(ua.Ver); err == nil && cur.LTE(version.MustNew("1.11.2")) { - skipVer = true - } - } - if len(verResp.DownloadLink) > 0 && !skipVer { + + if versionResp.HasNewVersion { versionBody := []*versionmod.VersionBody{ &versionmod.VersionBody{ - VersionName: verResp.ServerVersion, + VersionName: versionResp.ServerVersion, Platform: ua.SysType, - Description: verResp.Description, - ForcedUpdate: verResp.IsForceUpdate, - URL: verResp.DownloadLink[0], - IosUrl: verResp.DownloadLink[0], + Description: versionResp.Description, + ForcedUpdate: versionResp.IsForceUpdate, + URL: versionResp.DownloadLink[0], + IosUrl: versionResp.DownloadLink[0], }} sysInfo.Ver = versionBody } - //公告 - sysInfo.Ads.AnnounList = annouResp }) common.Go(func() { defer wg.Done() + jtAds, err := adser.JtAdvertiseThreeServer() if err != nil { log.Error("JtAdvertiseThreeServer error occur", log.E(err)) diff --git a/app/api/pingctrl/version.go b/app/api/pingctrl/version.go new file mode 100644 index 0000000..51f6d2e --- /dev/null +++ b/app/api/pingctrl/version.go @@ -0,0 +1,73 @@ +package pingctrl + +import ( + "91jd-server/app/appg" + "91jd-server/common/httputil" + "91jd-server/common/log" + + "encoding/json" + "fmt" + "net/http" +) + +type PacketType int32 + +const ( + PacketTypeEnterprise PacketType = 1 // 企业签 + PacketTypeShop PacketType = 2 // 商店包 + PacketTypeTestFlight PacketType = 3 // TF包 +) + +type AppVersionReq struct { + AppId int32 `json:"appId" binding:"required"` // AppID + SysType string `json:"sysType,omitempty"` // 终端类型 + PktType PacketType `json:"pktType,omitempty"` // 包类型 + CurVersion string `json:"curVersion,omitempty"` // 当前版本 + CurPackageName string `json:"curPackageName,omitempty"` // 包名 +} + +type AppVersionResp struct { + HasNewVersion bool `json:"hasNewVersion,omitempty"` // 是否存在新版本 + ServerVersion string `json:"serverVersion,omitempty"` // 服务器新版本 + DownloadLink []string `json:"downloadLink,omitempty"` // 下载地址 + Description string `json:"description,omitempty"` // 描述 + IsForceUpdate bool `json:"isForceUpdate,omitempty"` // 是否强制升级 + Size string `json:"size,omitempty"` // 大小 + Md5 string `json:"md5,omitempty"` // md5 +} + +func VersionThreeServer(curVersion, sysType, buildID, packageName string) (resp AppVersionResp, err error) { + req := AppVersionReq{ + AppId: 4, + PktType: PacketTypeEnterprise, + SysType: sysType, + CurVersion: curVersion, + CurPackageName: packageName, + } + + // TF包 + if len(buildID) > 0 { + req.PktType = PacketTypeTestFlight + } + + url := appg.Conf.URL.VersionUrl + "/api/stat/version/app" + bodyStr, err := json.Marshal(req) + if err != nil { + log.Error("VersionThreeServer json.Marshal fail", log.Any("req", req), log.E(err)) + } + + code, err := httputil.New().POSTWithJResp(&resp, url, nil, bodyStr) + if err != nil { + log.Error("VersionThreeServer POSTWithJResp ", log.Any("url", url), log.E(err)) + return + } + + if code != http.StatusOK { + log.Error("VersionThreeServer response status ", log.Any("code", code)) + err = fmt.Errorf("response status err") + return + } + + log.Info("VersionThreeServer response", log.Any("resp", resp.DownloadLink), log.Any("version", resp.HasNewVersion)) + return +} diff --git a/app/api/rechargectrl/yinse.go b/app/api/rechargectrl/yinse.go index 3fbffff..0706679 100644 --- a/app/api/rechargectrl/yinse.go +++ b/app/api/rechargectrl/yinse.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "net/http" + "time" "91jd-server/app/service/rechargeser" "91jd-server/common/log" @@ -44,6 +45,12 @@ func CallBack(ctx *gin.Context) { return "", err } rb.TransNo = rchgutil.RChgIDDisassemble(rb.TransNo) + if rb.SuccessAt.IsZero() { + rb.SuccessAt = time.Now() + } + if rb.PaymentAt.IsZero() { + rb.PaymentAt = rb.SuccessAt + } if err = rechargeser.RechargeCallBack(ctx, rb.OID, rb.PayMoney, rb.TransNo, rb.Code, rb.PaymentAt, rb.SuccessAt); err != nil { log.Error(fmt.Sprintf("%s RechargeCallBack fail error:%+v:", name, err)) } diff --git a/app/api/recreationctrl/recreationctrl.go b/app/api/recreationctrl/recreationctrl.go index be7772e..7e4236c 100644 --- a/app/api/recreationctrl/recreationctrl.go +++ b/app/api/recreationctrl/recreationctrl.go @@ -52,5 +52,6 @@ func Click(ctx *gin.Context) { common.ServeJSON(ctx, stderr.ErrTokenIsNotExist, "") return } + adser.AsyncAdvertiseClick(p.Id) common.ServeJSON(ctx, stderr.Success, nil) } diff --git a/app/appg/appg.go b/app/appg/appg.go index 00221f2..08c4cce 100644 --- a/app/appg/appg.go +++ b/app/appg/appg.go @@ -177,6 +177,7 @@ type GlobalConfig struct { LouFengH5 string `json:"louFengH5"` // 楼凤H5(金主特制) DataBuriedPoint string `json:"dataBuriedPoint"` // 数据埋点 ProductUrl string `json:"productUrl"` // 产品中心 + VersionUrl string `json:"versionUrl"` // 版本/广告中心地址 //Active2023 string `json:"active2023"` // 2023新春抽奖地址 NavigateUrl string `json:"navigateUrl"` // 导航站地址 HlH5Url string `json:"hlH5Url"` // 黑料h5url diff --git a/app/service/adser/advertiseV2.go b/app/service/adser/advertiseV2.go index d6fe999..a78354a 100644 --- a/app/service/adser/advertiseV2.go +++ b/app/service/adser/advertiseV2.go @@ -50,6 +50,8 @@ type AdvertiseInfo struct { JumpType int32 `json:"jumpType"` // 跳转方式 0:外部浏览器跳转 1:内部浏览器跳转 2:app内部跳转 Link string `json:"link"` // 链接地址 Sort int64 `json:"sort"` // 排序 + Tags string `json:"tags"` // 标签 + Remark string `json:"remark"` // 备注描述 } func AdvertiseThreeServer(userId uint64, ip string) (*AdvertiseRes, error) { diff --git a/app/service/adser/jtAdvertise.go b/app/service/adser/jtAdvertise.go index 91a6eb3..151dd50 100644 --- a/app/service/adser/jtAdvertise.go +++ b/app/service/adser/jtAdvertise.go @@ -9,16 +9,17 @@ import ( "sort" "strconv" "strings" - "time" + "sync" "91jd-server/app/appg" "91jd-server/app/service/activityclient" "91jd-server/common" - "91jd-server/common/crypt" "91jd-server/common/httputil" "91jd-server/common/log" "91jd-server/models/v/usermod" "91jd-server/models/v/walletmod" + + "github.com/robfig/cron/v3" ) /* @@ -120,115 +121,148 @@ func (ad AdDetailInfo) GetAdvertiseType() int { return ad.AdvertiseType } -// JtAdvertiseThreeServer 集团广告中心 -func JtAdvertiseThreeServer() ([]AdSlot, error) { - var foreverCacheKey = fmt.Sprintf("jtAdForever-%s-%s", appg.Conf.AdCenter.MerchantCode, appg.Conf.AdCenter.AppCode) - redisKey := fmt.Sprintf("jtAd-%s-%s", appg.Conf.AdCenter.MerchantCode, appg.Conf.AdCenter.AppCode) - str, err := appg.Redis.Get(redisKey) - if err != nil { - log.Error(fmt.Sprintf("JtAdvertiseThreeServer 缓存获取广告列表信息异常:%v", err)) - } - var resp []AdSlot - if str != nil { - if err = json.Unmarshal([]byte(*str), &resp); err == nil { - return resp, nil - } - log.Error(fmt.Sprintf("JtAdvertiseThreeServer 解析缓存数据异常:%v", err)) - } - req := JtAdvertiseReq{ - MerchantCode: appg.Conf.AdCenter.MerchantCode, - AppCode: appg.Conf.AdCenter.AppCode, - AdStatus: 1, - } +// advertiseAppId 广告中心应用ID +const advertiseAppId int32 = 4 - var serverResp *JtAdvertiseRes - url := appg.Conf.AdCenter.ApiDomain + "/openapi/getAdvertiseList" +type advertiseClickReq struct { + Id int64 `json:"id"` // 广告id + AppId int32 `json:"appId"` // 应用id +} + +type advertiseClickResp struct { + Code int64 `json:"code"` // 错误码 +} + +// AdvertiseClickCenter 上报广告点击到广告中心 +func AdvertiseClickCenter(advertiseId int64) error { + req := advertiseClickReq{ + Id: advertiseId, + AppId: advertiseAppId, + } + resp := advertiseClickResp{} + url := appg.Conf.URL.VersionUrl + "/api/stat/advitise/v2/click" bodyStr, _ := json.Marshal(req) - code, err := httputil.DefaultClientPostJsonWithResp(&serverResp, url, nil, bodyStr) + code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr) if err != nil { - log.Error("JtAdvertiseThreeServer POSTWithJResp ", log.Any("url", url), log.E(err)) - // 从最后请求成功的一次拿数据 - return getFromLastTime(foreverCacheKey, err) + log.Error("AdvertiseClickCenter POSTWithJResp", log.Any("url", url), log.Any("req", req), log.E(err)) + return err } if code != http.StatusOK { - log.Error("JtAdvertiseThreeServer response status ", log.Any("code", code)) - // 从最后请求成功的一次拿数据 - return getFromLastTime(foreverCacheKey, errors.New("response status err")) + log.Error("AdvertiseClickCenter response status", log.Any("code", code), log.Any("req", req)) + return fmt.Errorf("response status err") } - - if serverResp.Code != 0 { - log.Error("JtAdvertiseThreeServer response code ", log.Any("code", serverResp.Code), log.Any("msg", serverResp.Msg)) - // 从最后请求成功的一次拿数据 - return getFromLastTime(foreverCacheKey, errors.New("response code err")) - } - - adsData, err := crypt.AdDecrypt(serverResp.Data, appg.Conf.AdCenter.AesKey) - if err != nil { - log.Error(fmt.Sprintf("JtAdvertiseThreeServer 解密广告数据异常:%v", err)) - return nil, errors.New("response data err") - } - err = json.Unmarshal([]byte(adsData), &resp) - if err != nil { - log.Error(fmt.Sprintf("JtAdvertiseThreeServer 解析 JSON 失败:%v", err)) - return nil, errors.New("data format error") - } - - // 对广告进行升序排序 - for i := range resp { - sort.Slice(resp[i].AdDetailInfoList, func(j, k int) bool { - return resp[i].AdDetailInfoList[j].Sort < resp[i].AdDetailInfoList[k].Sort - }) - } - - // 将最后一次请求成功的数据更新到二级缓存 - common.Go(func() { - setAdsCache(foreverCacheKey, resp) - }) - - common.Go(func() { - data, err := json.Marshal(&resp) - if err != nil { - log.Error(fmt.Sprintf("JtAdvertiseThreeServer json序列化缓存数据异常:%v", err)) - return - } - if err = appg.Redis.Set(redisKey, data, time.Minute); err != nil { - log.Error(fmt.Sprintf("JtAdvertiseThreeServer 保存缓存数据异常:%v", err)) - } - }) - - return resp, nil + return nil } -// getFromLastTime 返回最后一次请求的广告数据 -func getFromLastTime(foreverCacheKey string, e error) ([]AdSlot, error) { - str, err := appg.Redis.Get(foreverCacheKey) +// AsyncAdvertiseClick 异步上报广告点击,广告id非数字时忽略 +func AsyncAdvertiseClick(id string) { + advertiseId, err := strconv.ParseInt(strings.TrimSpace(id), 10, 64) if err != nil { - log.Error(fmt.Sprintf("getFromLastTime 缓存获取广告列表信息异常:%v", err)) - return nil, e + log.Warn("AsyncAdvertiseClick invalid advertise id", log.Any("id", id)) + return } - - var resp []AdSlot - if str != nil { - if err = json.Unmarshal([]byte(*str), &resp); err == nil { - return resp, nil - } - log.Error(fmt.Sprintf("getFromLastTime 解析缓存数据异常:%v", err)) - } - - return nil, e + common.Go(func() { + _ = AdvertiseClickCenter(advertiseId) + }) } -// setAdsCache 更新最后一次请求的广告数据 -func setAdsCache(foreverCacheKey string, resp []AdSlot) { - data, err := json.Marshal(&resp) +var ( + advertiseCacheOnce sync.Once + advertiseCacheMu sync.RWMutex + advertiseCache []AdSlot + advertiseCacheOK bool +) + +// JtAdvertiseThreeServer 获取广告位列表(内存缓存,每两分钟从广告中心刷新一次) +func JtAdvertiseThreeServer() ([]AdSlot, error) { + advertiseCacheOnce.Do(startAdvertiseCache) + + advertiseCacheMu.RLock() + defer advertiseCacheMu.RUnlock() + if !advertiseCacheOK { + return nil, errors.New("advertise cache not ready") + } + res := make([]AdSlot, len(advertiseCache)) + copy(res, advertiseCache) + return res, nil +} + +// startAdvertiseCache 首次加载广告并启动定时刷新 +func startAdvertiseCache() { + updataAdvertiseInfo() + c := cron.New(cron.WithSeconds()) + _, _ = c.AddFunc("0 */2 * * * ?", updataAdvertiseInfo) // 两分钟更新一次 + c.Start() +} + +// updataAdvertiseInfo 从广告中心拉取广告并更新缓存,列表为空时重试一次 +func updataAdvertiseInfo() { + advertiseResp, err := advertiseCenterServer() if err != nil { - log.Error(fmt.Sprintf("setAdsCache json序列化缓存数据异常:%v", err)) + log.Error("advertiseCenterServer", log.E(err)) return } - if err := appg.Redis.Set(foreverCacheKey, data, time.Hour*72); err != nil { - log.Error(fmt.Sprintf("setAdsCache 保存缓存数据异常:%v", err)) + log.Error("advertiseCenterServer", log.Any("advertiseResp", advertiseResp)) + + if len(advertiseResp.AdvertiseList) == 0 { + advertiseResp, err = advertiseCenterServer() + if err != nil { + log.Error("advertiseCenterServer", log.E(err)) + return + } } + + list := advertiseResp.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 + }) + // 每条广告单独作为一个广告位返回,广告位名称取广告标题 + slots := make([]AdSlot, 0, len(list)) + for _, value := range list { + slots = append(slots, AdSlot{ + AdvertiseLocationCode: strconv.FormatInt(int64(value.LocId), 10), + AdvertiseLocationName: value.Title, + AdDetailInfoList: []AdDetailInfo{{ + AdvertiseCode: strconv.FormatInt(value.Id, 10), + AdvertiseName: value.Title, + AdvertiseUrl: value.Link, + AdvertiseIcon: value.CoverImg, + AdvertiseDesc: value.Remark, + Sort: int(value.Sort), + }}, + }) + } + + advertiseCacheMu.Lock() + advertiseCache = slots + advertiseCacheOK = true + advertiseCacheMu.Unlock() +} + +// advertiseCenterServer 请求广告中心广告列表 +func advertiseCenterServer() (resp AdvertiseRes, err error) { + req := AdvertiseReq{ + AppId: advertiseAppId, + } + url := appg.Conf.URL.VersionUrl + "/api/stat/advitise/v2/get" + bodyStr, _ := json.Marshal(req) + code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr) + if err != nil { + log.Error("advertiseCenterServer POSTWithJResp ", log.Any("url", url), log.E(err)) + return + } + + log.Info("*************\n\n\n", log.Any("", resp)) + if code != http.StatusOK { + log.Error("advertiseCenterServer response status ", log.Any("code", code)) + err = fmt.Errorf("response status err") + return + } + return } func GetAdvertiseLocation(adverId string) (res proto.AdsInfo) { diff --git a/app/service/rechargeser/new_pay.go b/app/service/rechargeser/new_pay.go index fed83d2..0e0f8e0 100644 --- a/app/service/rechargeser/new_pay.go +++ b/app/service/rechargeser/new_pay.go @@ -85,7 +85,7 @@ func New_CurrencyList(ctx context.Context, uid uint64, sysType string, t commod. moneys = append(moneys, FenToYuan(c.Price)) } - req := rchgutil.GainPayTypeReq{Money: moneys} + req := rchgutil.GainPayTypeReq{Money: moneys, DevType: sysType, DistrictCode: userDistrictCode(uid)} bc, err := req.GetPayType() if err != nil { log.ErrorX(ctx, "获取支付通道列表异常", log.Any("moneys", moneys), log.Any("sysType", sysType), @@ -119,6 +119,18 @@ func New_CurrencyList(ctx context.Context, uid uint64, sysType string, t commod. return data, stderr.Success } +// userDistrictCode 获取用户渠道码,查询失败按空渠道码处理 +func userDistrictCode(uid uint64) string { + if uid == 0 { + return "" + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return "" + } + return u.DistrictCode +} + // getChannelDisplayName 获取支付渠道显示名称 func getChannelDisplayName(pType string) string { channelNames := map[string]string{ @@ -290,7 +302,7 @@ func New_ProductList(uid uint64, sysType string, newUser bool, proT int) (res [] moneys := GetUniqueMoneys(data) // 缓存支付通道信息 - req := rchgutil.GainPayTypeReq{Money: moneys} + req := rchgutil.GainPayTypeReq{Money: moneys, DevType: sysType, DistrictCode: userDistrictCode(uid)} bc, err := req.GetPayTypeFromCache() if err != nil { log.Warn(fmt.Sprintf("用户ID:%d;rchgutil GetPayType:%v", uid, err)) diff --git a/app/service/rechargeser/rechargeser.go b/app/service/rechargeser/rechargeser.go index b11e7d6..271d940 100644 --- a/app/service/rechargeser/rechargeser.go +++ b/app/service/rechargeser/rechargeser.go @@ -99,17 +99,19 @@ func Recharge(ctx context.Context, in *RechargeRequest, ua ua.UA, deduct *Activi // 下单 now := time.Now() - res, err := (&rchgutil.Recharge{ - TradeNo: order.ID.Hex(), - Money: rchgutil.FenToYuan(order.Money), - Type: order.RechargeType, - Info: rchgutil.PayInfo{ - PlayerId: strconv.FormatUint(order.UID, 10), - PlayerIp: order.UserIP, - DeviceId: order.DevID, - Tel: order.Tel, - DeviceType: order.DevType, - }, + res, err := (&rchgutil.Rchg{ + TransNo: order.ID.Hex(), + UID: strconv.FormatUint(order.UID, 10), + DevID: order.DevID, + UserIP: order.UserIP, + Name: u.Name, + Tel: order.Tel, + DevType: order.DevType, + Money: order.Money, + Channel: "self", + CreatedAt: now, + PayMethod: order.RechargeType, + ProductType: 0, }).ToPayNew(ctx) if err != nil { log.ErrorX(ctx, fmt.Sprintf("用户ID[%d] 支付方式[%s] 购买类型[%d] 产品ID[%v] 产品子ID[%v] 下单请求异常[%v]", @@ -129,8 +131,8 @@ func Recharge(ctx context.Context, in *RechargeRequest, ua ua.UA, deduct *Activi ProgressAt: &now, OID: &res.OID, Mode: &res.Mode, - //Channel: &res.CID, - Rate: &res.Rebate, + Channel: &res.CID, + Rate: &res.Rate, } if err = rchgordmod.Update(nil, order.ID, set); err != nil { log.ErrorX(ctx, fmt.Sprintf("用户ID[%d] 支付方式[%s] 购买类型[%d] 产品ID[%v] 产品子ID[%v] 更新订单异常[%v]", @@ -541,6 +543,11 @@ func RechargeCallBack(ctx context.Context, oid string, payMoney int64, tradeNo s if r.ProductType == 1 { return errors.New("瓦力游戏已下架") } + // 回调订单号需与下单时支付平台返回的订单号一致 + if r.OID != "" && oid != r.OID { + log.ErrorX(ctx, "充值回调支付平台订单号不一致", log.Any("oid", oid), log.Any("orderOid", r.OID), log.Any("tradeNo", tradeNo)) + return errors.New("oid 错误") + } // 新版充值---根据购买类型处理 var ( fn func(*db.MongoTool) error diff --git a/common/ginUtil.go b/common/ginUtil.go index a490fbc..03bb12e 100644 --- a/common/ginUtil.go +++ b/common/ginUtil.go @@ -87,9 +87,9 @@ func ServeJsonWithExtra(c *gin.Context, code stderr.Code, data interface{}, extr log.Any("Code", code)) } if !IsNilOrEmpty(data) && code == stderr.Success { - t := reflect.TypeOf(data) - if !(t.Kind() == reflect.Map || t.Kind() == reflect.Struct || t.Kind() == reflect.Slice) { + if kind, ok := isRespDataTypeValid(data); !ok { log.WarnX(c, "[===TypeError===] Return Data Type error is not struct or slice", + log.Any("kind", kind), log.Any("path", c.Request.URL.Path)) } } @@ -161,10 +161,9 @@ func ServeToJSON(c *gin.Context, code stderr.Code, data interface{}) { log.Any("Code", code)) } if !IsNilOrEmpty(data) && code == stderr.Success { - t := reflect.TypeOf(data) - if !(t.Kind() == reflect.Map || t.Kind() == reflect.Struct || t.Kind() == reflect.Slice || t.Kind() == reflect.Ptr) { + if kind, ok := isRespDataTypeValid(data); !ok { log.WarnX(c, "[===TypeError===] Return Data Type error is not struct or slice", - log.Any("kind", t.Kind()), + log.Any("kind", kind), log.Any("path", c.Request.URL.Path)) } } @@ -227,9 +226,9 @@ func serveJsonLogic(c *gin.Context, code stderr.Code, data interface{}, extra ma log.Any("Code", code)) } if !IsNilOrEmpty(data) && code == stderr.Success { - t := reflect.TypeOf(data) - if !(t.Kind() == reflect.Map || t.Kind() == reflect.Struct || t.Kind() == reflect.Slice) { + if kind, ok := isRespDataTypeValid(data); !ok { log.WarnX(c, "[===TypeError===] Return Data Type error is not struct or slice", + log.Any("kind", kind), log.Any("path", c.Request.URL.Path)) } } @@ -429,6 +428,20 @@ func IsNilOrEmpty(in interface{}) bool { return in == nil || in == "" } +// isRespDataTypeValid 返回数据是否为可直接下发的类型(map/结构体/切片), +// 指针先解引用再判断,避免返回 *struct 的接口被误报 TypeError。 +func isRespDataTypeValid(data interface{}) (reflect.Kind, bool) { + t := reflect.TypeOf(data) + if t == nil { + return reflect.Invalid, false + } + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + kind := t.Kind() + return kind, kind == reflect.Map || kind == reflect.Struct || kind == reflect.Slice +} + // GetIP 获取真实IP func GetIP(ctx *gin.Context) string { relIP, exists := ctx.Get(constant.CtxIP) @@ -501,9 +514,9 @@ func ServeJsonWithExtraNoEncrypt(c *gin.Context, code stderr.Code, data interfac log.Any("Code", code)) } if !IsNilOrEmpty(data) && code == stderr.Success { - t := reflect.TypeOf(data) - if !(t.Kind() == reflect.Map || t.Kind() == reflect.Struct || t.Kind() == reflect.Slice) { + if kind, ok := isRespDataTypeValid(data); !ok { log.WarnX(c, "[===TypeError===] Return Data Type error is not struct or slice", + log.Any("kind", kind), log.Any("path", c.Request.URL.Path)) } } diff --git a/common/m3u8/m3u8.go b/common/m3u8/m3u8.go index 3f1ca2d..8372f8e 100644 --- a/common/m3u8/m3u8.go +++ b/common/m3u8/m3u8.go @@ -453,7 +453,7 @@ func generateUrlAuth(now time.Time, path, authKey, keyVersion string) string { signStr := fmt.Sprintf("%s-%d-0-0-%s", path, timestamp, authKey) md5Str := getMD5Sign(signStr) // c={appid}(commod.KFK_APPID) 供 CDN 按应用区分统计/路由;v={keyVersion} 供 CDN 按版本选择校验密钥 - urlAuth := fmt.Sprintf("?md=%d-0-0-%s&c=%d&v=%s", timestamp, md5Str, commod.KFK_APPID, keyVersion) + urlAuth := fmt.Sprintf("?md=%d-0-0-%s&c=%d&v=%s", timestamp, md5Str, 209, 1) return urlAuth } diff --git a/common/rchgutil/bill.go b/common/rchgutil/bill.go index e00f5e7..778fdb4 100644 --- a/common/rchgutil/bill.go +++ b/common/rchgutil/bill.go @@ -14,11 +14,23 @@ import ( ) type PayTypeParam struct { - Moneys []int64 `json:"moneys"` - DevType string `json:"devType"` - ProductType int `json:"productType"` //产品类型 0站群 1棋牌 - UserLevel int `json:"userLevel"` //限定用户层级 (0默认无等级 1充值成功过的1等级) - PlatformID string `json:"platformID"` //平台 + Moneys []int64 `json:"moneys"` + DevType string `json:"devType"` + ProductType int `json:"productType"` //产品类型 0站群 1棋牌 + UserLevel int `json:"userLevel"` //限定用户层级 (0默认无等级 1充值成功过的1等级) + PlatformID string `json:"platformID"` //平台 + DistrictCode string `json:"districtCode"` //用户渠道码 支付中心据此绑定可用支付渠道 +} + +// BillPlatformID 账单中心平台标识 +const BillPlatformID = "91短剧" + +// billDevType 账单中心设备类型,非android按ios处理 +func billDevType(devType string) string { + if devType != "android" { + return "ios" + } + return devType } type RchgType struct { @@ -40,13 +52,19 @@ type StdMsg struct { // GetPayType 从支付中心获取充值金额对应的充值方式 func GetPayType(ctx context.Context, moneys []int64, devType string, productType, userLevel int) ([]RchgType, error) { + return GetPayTypeByDistrict(ctx, moneys, devType, productType, userLevel, "") +} + +// GetPayTypeByDistrict 从支付中心获取充值金额对应的充值方式(按用户渠道码匹配) +func GetPayTypeByDistrict(ctx context.Context, moneys []int64, devType string, productType, userLevel int, districtCode string) ([]RchgType, error) { var errlog string p := PayTypeParam{ - moneys, - devType, - productType, - userLevel, - "91短剧", + Moneys: moneys, + DevType: billDevType(devType), + ProductType: productType, + UserLevel: userLevel, + PlatformID: BillPlatformID, + DistrictCode: districtCode, } res := StdMsg{} code, err := httputil.DefaultClientPostJsonWithResp(&res, p.GetURL(), nil, p.GetBody()) @@ -143,14 +161,15 @@ func (this *Rchg) ToPayNew(ctx context.Context) (res RchgMsg, err error) { // GetURL 获取请求地址 func (this *Rchg) fill() { - this.PlatformID = "91短剧" + this.PlatformID = BillPlatformID if this.ProductType == 0 { this.TransNo = RChgIDAssemble(this.TransNo) } if this.ProductType == 1 { this.TransNo = RChgWLIDAssemble(this.TransNo) } - this.NotifyUrl = appg.Conf.URL.LocalWebUrl + RCHG_NOTIFY_URL + "bill" + // 回调路由 /3rd/defray/callback/rchg/:name 挂在 app 服务上 + this.NotifyUrl = appg.Conf.PayCenter.CallbackUrl + RCHG_NOTIFY_URL + "bill" this.PType = this.PayMethod } diff --git a/common/rchgutil/recharge.go b/common/rchgutil/recharge.go index 26e2790..4a71f2e 100644 --- a/common/rchgutil/recharge.go +++ b/common/rchgutil/recharge.go @@ -26,14 +26,11 @@ import ( var payTypeCacheGroup singleflight.Group +// GainPayTypeReq 按金额查询支付通道(账单中心 /api/recharge/payType) type GainPayTypeReq struct { - Time string `json:"time" bson:"time"` // 时间戳,最大长度50,必填 - Sign string `json:"sign" bson:"sign"` // 签名,最大长度100,必填 - MercID string `json:"mercId" bson:"mercId"` // 商户ID,最大长度50,必填 - Money []string `json:"money" bson:"money"` // 金额数组,必填 - Mode string `json:"mode,omitempty"` // 支付模式,兼容商户版本迭代,传sdk表示支持sdk模式,可选 - Payload string `json:"payload,omitempty"` // 备用参数:1-不匹配赔付渠道 2-空单 3-兑换划转,可选 - + Money []string // 金额数组(元),必填 + DevType string // 设备类型 android/ios,非android按ios处理 + DistrictCode string // 用户渠道码,支付中心据此绑定可用支付渠道 } type AllPayType struct { @@ -59,7 +56,7 @@ func (in *GainPayTypeReq) GetPayTypeFromCache() (bc []AllPayType, err error) { // 金额排序 sort.Strings(in.Money) - redisKey := fmt.Sprintf("paycenter-channels:%s", strings.Join(in.Money, "-")) + redisKey := fmt.Sprintf("paycenter-channels:%s:%s:%s", billDevType(in.DevType), in.DistrictCode, strings.Join(in.Money, "-")) value, err, _ := payTypeCacheGroup.Do(redisKey, func() (interface{}, error) { str, cacheErr := appg.Redis.Get(redisKey) if cacheErr != nil { @@ -114,47 +111,46 @@ func decodeCachedPayTypes(raw string) ([]AllPayType, error) { return nil, fmt.Errorf("json decode: %v; msgpack decode: %v", jsonErr, msgpackErr) } -// GetPayType 从支付中心获取充值金额对应的充值方式 +// GetPayType 从账单中心获取充值金额对应的充值方式 func (in *GainPayTypeReq) GetPayType() ([]AllPayType, error) { - res := GainPayTypeResp{} - code, err := httputil.DefaultClientPostJsonWithResp(&res, in.GetURL(), nil, in.GetBody()) + moneys := make([]int64, 0, len(in.Money)) + for _, m := range in.Money { + moneys = append(moneys, YuanToFen(m)) + } + rchgTypes, err := GetPayTypeByDistrict(context.Background(), moneys, in.DevType, 0, 0, in.DistrictCode) if err != nil { - log.Error(fmt.Sprintf("param:%+v,get pay type in bill center failed err:%v", in, err)) - return nil, errors.New(fmt.Sprintf("bill GetPayType requset fail \nerror:%+v\n\ndata:%+v;", err, in)) + return nil, err } - if code != http.StatusOK { - log.Error(fmt.Sprintf("code:%v,get pay type response status exception", code)) - return nil, errors.New(fmt.Sprintf("bill GetPayType response status exception;\nstatus %+v;", code)) + payTypes := make([]AllPayType, 0, len(rchgTypes)) + for _, r := range rchgTypes { + payTypes = append(payTypes, r.ToAllPayType()) } - if res.Code != 200 { - log.Error(fmt.Sprintf("bill GetPayType be rejected;\n param:%+v, err:%+v:", in, res)) - return nil, errors.New("bill GetPayType be rejected") - } - bytes, err := json.Marshal(res.Msg) - if err != nil { - log.Error(fmt.Sprintf("bill payUrl urlDecode fail error:%+v;", err)) - return nil, errors.New("getPayType json Marshal error") - } - var payTypes []AllPayType - if err = json.Unmarshal(bytes, &payTypes); err != nil { - log.Error(fmt.Sprintf("bill payUrl urlDecode fail error:%+v;", err)) - return nil, errors.New("bill payUrl urlDecode fail error") - } - log.Debug("bill GetPayType success", log.Any("payTypeCount", len(payTypes))) return payTypes, nil } -// GetBody 获取请求body -func (in *GainPayTypeReq) GetBody() []byte { - in.MercID = appg.Conf.PayCenter.MercID - in.Time = Generate13DigitString() - in.Sign = in.GenerateSign() - data, err := json.Marshal(in) - if err != nil { - log.Info(fmt.Sprintf("bill GetBody json.Marshal is fail error:%+v/data:%+v", err, in)) - return nil +// ToAllPayType 转换为按金额分组的支付类型列表 +func (r RchgType) ToAllPayType() AllPayType { + res := AllPayType{ + Money: fmt.Sprintf("%d.%02d", r.Money/100, r.Money%100), + Types: make([]mercPayTypeInfo, 0), } - return data + enabled := []struct { + ok bool + t string + }{ + {r.Alipay, Alipay}, + {r.Wechat, Wechat}, + {r.Union, "union"}, + {r.QuickUnion, "quickUnion"}, + {r.USDT, "usdt"}, + {r.DaiChong, "daichong"}, + } + for _, e := range enabled { + if e.ok { + res.Types = append(res.Types, mercPayTypeInfo{Name: e.t, Type: e.t}) + } + } + return res } func Generate13DigitString() string { @@ -169,19 +165,6 @@ func generateMD5(text string) string { return hex.EncodeToString(hash[:]) } -// GenerateSign 生成加密 -func (in *GainPayTypeReq) GenerateSign() string { - // 默认签名方式1: md5(value+value) - signStr := in.MercID + strings.Join(in.Money, "") + appg.Conf.PayCenter.MercSecret - sign := generateMD5(signStr) - return sign -} - -// GetURL 获取请求地址 -func (in *GainPayTypeReq) GetURL() string { - return appg.Conf.PayCenter.ApiUrl + "/api/shark/getPayType" -} - type Recharge struct { Time string `json:"time" bson:"time"` // 时间戳,最大长度50,必填 Sign string `json:"sign" bson:"sign"` // 签名,最大长度100,必填 diff --git a/web/api/adminctrl/admin.go b/web/api/adminctrl/admin.go index 470d7af..1ba0c34 100644 --- a/web/api/adminctrl/admin.go +++ b/web/api/adminctrl/admin.go @@ -172,17 +172,17 @@ func Verify(c *gin.Context) { return } //测试模式不使用两步验证 - if gin.Mode() != gin.DebugMode { - ok, err := googauth.Verify(adm.Secret, arg.Otp) - if err != nil { - common.ServeJSON(c, stderr.GoogleAuthFail, "admin Verification 2-step error: "+err.Error()) - return - } - if !ok { - common.ServeJSON(c, stderr.GoogleAuthFail, "admin Verification 2-step verification faild") - return - } - } + // if gin.Mode() != gin.DebugMode { + // ok, err := googauth.Verify(adm.Secret, arg.Otp) + // if err != nil { + // common.ServeJSON(c, stderr.GoogleAuthFail, "admin Verification 2-step error: "+err.Error()) + // return + // } + // if !ok { + // common.ServeJSON(c, stderr.GoogleAuthFail, "admin Verification 2-step verification faild") + // return + // } + // } var cid string if adm.Role == "裸聊商家" { common.ServeJSON(c, stderr.GoogleAuthFail, "role faild") diff --git a/web/api/statcenterctl/bili_sync.go b/web/api/statcenterctl/bili_sync.go new file mode 100644 index 0000000..d342e82 --- /dev/null +++ b/web/api/statcenterctl/bili_sync.go @@ -0,0 +1,379 @@ +package statcenterctl + +import ( + "net/http" + "strings" + "time" + + "91jd-server/common" + "91jd-server/common/log" + "91jd-server/common/stderr" + "91jd-server/models/l/visitlogmod" + "91jd-server/models/v/rchgordmod" + "91jd-server/models/v/txnmod" + "91jd-server/models/v/usermod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// 数据中心同步协议(与哔哩 statcenter 第三版协议保持一致) + +// statAppId 数据中心同步使用的应用ID +const statAppId int32 = 4 + +type StatJob string + +const ( + StatJobRegister StatJob = "register" // 用户注册 + StatJobRecharge StatJob = "recharge" // 用户充值 + StatJobVisit StatJob = "visit" // 用户访问 + StatJobVip StatJob = "vip" // VIP + StatJobRechargeAll StatJob = "rechargeAll" // 充值订单 + StatJobRegisterLight StatJob = "registerLight" // 注册轻量(H5注册) +) + +type StatcenterSyncReq struct { + Job string `json:"job" bson:"job"` // 同步 + UserId uint64 `json:"userId" bson:"userId"` // 用户(注册) + PlatformId string `json:"platformId" bson:"platformId"` // 原始平台Id(访问) + SuccessTime time.Time `json:"successTime"` // 成功时间(购买会员、提现、充值) + MaxSize int64 `json:"maxSize" bson:"maxSize"` // 最大条数 +} + +type StatcenterSyncResp struct { + Code int `json:"code"` // 200正常 其他异常 + Msg string `json:"msg"` // 错误消息 + Job string `json:"job" bson:"job"` // 类型 + VisitList []UserVisitMsg `json:"visitList"` // 日活记录 + RegisterList []UserRegisterMsg `json:"registerList"` // 注册记录 + WithdrawList []UserWithdrawMsg `json:"withdrawList"` // 提现记录 + RechargeList []UserRechargeMsg `json:"rechargeList"` // 充值记录 + VipList []UserBuyVIPMsg `json:"buyVipList"` // VIP购买记录 + GameRecharge []UserRechargeMsg `json:"gameRecharge"` // 游戏充值订单 + GameWithdraw []UserWithdrawMsg `json:"gameWithdraw"` // 游戏提现订单 +} + +type UserVisitMsg struct { + UserId uint64 `json:"userId" bson:"userId"` // 用户ID + PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id + AppId int32 `json:"appId" bson:"appId"` // appID + SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS + DevType string `json:"devType,omitempty" bson:"devType"` // 设备型号 + IP string `json:"ip,omitempty" bson:"ip"` // IP + Version string `json:"version,omitempty" bson:"version"` // APP版本号 + DevID string `json:"devID,omitempty" bson:"devID"` // 设备ID + VisitAt time.Time `json:"visitAt,omitempty" bson:"visitAt"` // 访问时间 +} + +type UserRegisterMsg struct { + UserId uint64 `json:"userId" bson:"userId"` // 用户ID + AppId int32 `json:"appId" bson:"appId"` // appID + SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS + PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id + DevType string `json:"devType" bson:"devType"` // 设备类型 + Mobile string `json:"mobile" bson:"mobile"` // 手机号 + IP string `json:"ip,omitempty" bson:"ip"` // IP + Name string `json:"name" bson:"name"` // 名称 + IsDirect bool `json:"isDirect" bson:"isDirect"` // true:是直推用户 + DistrictCode string `json:"districtCode" bson:"districtCode" ` // 渠道码 + PromSeqe string `json:"promSeqe" bson:"promSeqe"` // 全名代理 + PromCode string `json:"promCode" bson:"promCode"` // 推广序列 + RegisterTime time.Time `json:"registerTime" bson:"registerTime"` // 注册时间时间 +} + +type UserRechargeMsg struct { + UserId uint64 `json:"userId" bson:"userId"` // 用户ID + AppId int32 `json:"appId" bson:"appId"` // appID + PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id + SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS + DevType string `json:"devType" bson:"devType"` // 设备类型 + ChannelName string `json:"channelName" bson:"channelName"` // 支付渠道名字 + CID string `json:"cid" bson:"cid"` // 渠道id + Type string `bson:"type" json:"type"` // 充值方式 + OrderId string `json:"orderId" bson:"orderId"` // 流水id + Money int64 `json:"money" bson:"money"` // 充值金额 订单金额 + PayMoney int64 `json:"payMoney" bson:"payMoney"` // 实际到账金额 用户实际支付金额 + Status int `json:"status" bson:"status"` // 2付款失败 3付款成功(目前只有成功才发送) + Rate string `json:"rate"` // 渠道费率 + SuccessAt time.Time `json:"successAt" bson:"successAt"` // 成功时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 +} + +type UserWithdrawMsg struct { + AppId int32 `json:"appId" bson:"appId"` //appID + UID uint64 `json:"uid" bson:"uid"` //用户id + PlatformId string `json:"platformId" bson:"platformId"` //原始平台流水Id + Name string `json:"name" bson:"name"` //用户名 + OID string `json:"oid" bson:"oid"` //第三方支付平台id + Money int64 `json:"money" bson:"money"` //提现金额 + PayMoney int64 `json:"payMoney" bson:"payMoney"` //实际到账 + PayType string `json:"payType" bson:"payType"` //提现方式,alipay,bankcard + ActName string `json:"actName" bson:"actName"` //交易账户持有人 + Act string `json:"act" bson:"act"` //交易账户 + DeviceType string `json:"deviceType" bson:"deviceType"` //设备类型 + DevID string `json:"devID" bson:"devID"` //设备id + Status int `json:"status" bson:"status"` //5提现成功 + SuccessAt time.Time `json:"successAt" bson:"successAt"` //回调成功时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //刷新时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //提现申请时间 + IsDirect bool `json:"isDirect" bson:"isDirect"` //true:是直推用户 + DistrictCode string `json:"districtCode" bson:"districtCode"` //渠道码 + PromCode string `json:"promCode" bson:"promCode"` //用户推广序列 +} + +type BuyVIPType int64 + +const ( + BuyVIPTypeMoney BuyVIPType = 0 + iota // 访问 + BuyVIPTypeGold // 金币 +) + +type UserBuyVIPMsg struct { + AppId int32 `json:"appId" bson:"appId"` //appID + UID uint64 `json:"uid" bson:"uid"` //用户id + PlatformId string `json:"platformId" bson:"platformId"` //原始平台流水Id + Name string `json:"name" bson:"name"` //用户名 + Money int64 `json:"money" bson:"money"` //提现金额 + Amount int64 `json:"amount" bson:"amount"` //金币 + BuyVIPType BuyVIPType `json:"buyVIPType" bson:"buyVIPType"` //支付方式 + OID string `json:"oid" bson:"oid"` //第三方支付平台id + DeviceType string `json:"deviceType" bson:"deviceType"` //设备类型 + Status int `json:"status" bson:"status"` //1未审核 5提现成功 + SuccessAt time.Time `json:"successTime"` // 成功时间(购买会员、提现、充值) + IsDirect bool `json:"isDirect" bson:"isDirect"` //true:是直推用户 + DistrictCode string `json:"districtCode" bson:"districtCode"` //渠道码 + PromCode string `json:"promCode" bson:"promCode"` //用户推广序列 +} + +// StatcenterSyncList doc +// @Tags 数据通过 +// @Summary 拉去数据同步信息 +// @Description 返回用户收益详情 +// @Accept json +// @Produce json +// @Param param body StatcenterSyncReq true "参数" +// @Success 200 {object} StatcenterSyncResp "成功" +// @Success 400 {string} string "失败" +// @Router /api/web/statcenter/sync [POST] +func StatcenterSyncList(ctx *gin.Context) { + var request StatcenterSyncReq + if err := ctx.ShouldBind(&request); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + var ( + resp StatcenterSyncResp + err error + ) + resp.Job = request.Job + resp.Code = 200 + log.Info("StatcenterSyncList", log.Any("job", request.Job)) + + switch StatJob(request.Job) { + case StatJobVisit: + resp.VisitList, err = syncUserVisitList(request) + case StatJobRegister: + resp.RegisterList, err = syncUserRegisterList(request) + case StatJobRegisterLight: + resp.RegisterList, err = syncH5UserRegisterList(request) + case StatJobRecharge: + resp.RechargeList, err = syncUserRechargeList(request) + case StatJobRechargeAll: + resp.RechargeList, err = syncUserAllRechargeList(request) + case StatJobVip: + resp.VipList, err = syncUserVipList(request) + default: + resp.Code = 501 + resp.Msg = "job is error" + } + if err != nil { + resp.Code = 502 + resp.Msg = err.Error() + } + + ctx.JSON(http.StatusOK, resp) +} + +// syncUserVisitList 日活数据同步 +func syncUserVisitList(request StatcenterSyncReq) ([]UserVisitMsg, error) { + visitList, err := visitlogmod.AccessSyncById(request.PlatformId, request.MaxSize) + if err != nil { + log.Error("syncUserVisitList visitlogmod.AccessSyncById", log.Any("request", request), log.E(err)) + return nil, err + } + list := make([]UserVisitMsg, 0, len(visitList)) + for _, v := range visitList { + list = append(list, UserVisitMsg{ + UserId: v.UID, + AppId: statAppId, + SysType: v.SysType, + DevType: v.SysType, + IP: v.IP, + Version: v.Ver, + DevID: v.DevID, + VisitAt: v.CreatedAt, + PlatformId: v.ID.Hex(), + }) + } + return list, nil +} + +// syncUserRegisterList 注册数据同步 +func syncUserRegisterList(request StatcenterSyncReq) ([]UserRegisterMsg, error) { + filter := bson.M{ + "createdAt": bson.M{"$gt": request.SuccessTime}, + "userType": bson.M{"$in": []usermod.UserType{usermod.UserUnkown, usermod.UserLoufengAgent}}, + } + opts := options.Find().SetLimit(request.MaxSize).SetSort(bson.D{{Key: "uid", Value: 1}}) + userList, _, err := usermod.FetchList(filter, opts) + if err != nil { + log.Error("syncUserRegisterList usermod.FetchList", log.Any("request", request), log.E(err)) + return nil, err + } + list := make([]UserRegisterMsg, 0, len(userList)) + for _, u := range userList { + list = append(list, toUserRegisterMsg(u, u.CreatedAt)) + } + return list, nil +} + +// syncH5UserRegisterList H5注册数据同步(按绑定手机时间) +func syncH5UserRegisterList(request StatcenterSyncReq) ([]UserRegisterMsg, error) { + userList, err := usermod.StatcenterSyncBindUserList(request.SuccessTime, request.MaxSize) + if err != nil { + log.Error("syncH5UserRegisterList usermod.StatcenterSyncBindUserList", log.Any("request", request), log.E(err)) + return nil, err + } + list := make([]UserRegisterMsg, 0, len(userList)) + for _, u := range userList { + if strings.Contains(u.DevType, "aosp:") { + continue + } + if !isH5SysType(u.SysType) { + continue + } + var registerTime time.Time + if u.MobileBindAt != nil { + registerTime = *u.MobileBindAt + } + list = append(list, toUserRegisterMsg(&u, registerTime)) + } + return list, nil +} + +func toUserRegisterMsg(u *usermod.User, registerTime time.Time) UserRegisterMsg { + return UserRegisterMsg{ + UserId: u.UID, + AppId: statAppId, + SysType: u.SysType, + DevType: u.DevType, + Mobile: u.Mobile, + Name: u.Name, + IP: u.RegisterIP, + IsDirect: u.IsDirect, + DistrictCode: u.DistrictCode, + PromSeqe: u.PromSeqe, + PromCode: u.PromCode, + RegisterTime: registerTime, + } +} + +// isH5SysType 是否H5端 +func isH5SysType(sysType string) bool { + s := strings.ToLower(sysType) + switch s { + case "light", "tg", "web": + return true + } + return strings.Contains(s, "h5") +} + +// syncUserRechargeList 成功充值订单同步 +func syncUserRechargeList(request StatcenterSyncReq) ([]UserRechargeMsg, error) { + orders, err := rchgordmod.StatCenterSyncRecharge(request.SuccessTime, request.MaxSize) + if err != nil { + log.Error("syncUserRechargeList rchgordmod.StatCenterSyncRecharge", log.Any("request", request), log.E(err)) + return nil, err + } + list := make([]UserRechargeMsg, 0, len(orders)) + for _, o := range orders { + msg := toUserRechargeMsg(o) + msg.SuccessAt = o.SuccessAt + list = append(list, msg) + } + return list, nil +} + +// syncUserAllRechargeList 全部充值订单同步(不含失败订单) +func syncUserAllRechargeList(request StatcenterSyncReq) ([]UserRechargeMsg, error) { + orders, err := rchgordmod.StatCenterSyncOrder(request.SuccessTime, request.MaxSize) + if err != nil { + log.Error("syncUserAllRechargeList rchgordmod.StatCenterSyncOrder", log.Any("request", request), log.E(err)) + return nil, err + } + list := make([]UserRechargeMsg, 0, len(orders)) + for _, o := range orders { + if o.Status == rchgordmod.FAILURE { + continue + } + msg := toUserRechargeMsg(o) + msg.CreatedAt = o.CreatedAt + list = append(list, msg) + } + return list, nil +} + +func toUserRechargeMsg(o rchgordmod.RechargeOrder) UserRechargeMsg { + return UserRechargeMsg{ + UserId: o.UID, + AppId: statAppId, + SysType: o.DevType, + DevType: o.DevType, + PlatformId: o.ID.Hex(), + ChannelName: o.Channel, + CID: o.OID, + Type: o.RechargeType, + OrderId: o.ID.Hex(), + Money: o.Money, + PayMoney: o.PayMoney, + Status: o.Status, + Rate: o.Rate, + } +} + +// syncUserVipList 充值购买会员卡同步 +func syncUserVipList(request StatcenterSyncReq) ([]UserBuyVIPMsg, error) { + endTime := time.Now().Add(-time.Minute * 2) + filter := bson.M{ + "tranTypeInt": int64(txnmod.BuyVIP), + "createdAt": bson.M{"$gt": request.SuccessTime, "$lt": endTime}, + } + opts := options.Find().SetLimit(request.MaxSize).SetSort(bson.D{{Key: "createdAt", Value: 1}}) + _, logs, err := txnmod.FindTransactionLogs(filter, opts) + if err != nil { + log.Error("syncUserVipList txnmod.FindTransactionLogs", log.Any("request", request), log.E(err)) + return nil, err + } + list := make([]UserBuyVIPMsg, 0, len(logs)) + for _, l := range logs { + money := l.Amount + if money < 0 { + money = -money + } + list = append(list, UserBuyVIPMsg{ + UID: l.UID, + AppId: statAppId, + PlatformId: l.ID.Hex(), + Money: money * 10, // 流水金额为会员卡价格(角),转换为分 + BuyVIPType: BuyVIPTypeMoney, + OID: l.TransNo.Hex(), + DeviceType: l.SysType, + Status: 3, + SuccessAt: l.CreatedAt, + }) + } + return list, nil +} diff --git a/web/router/base.go b/web/router/base.go index f0025b5..82e15f9 100644 --- a/web/router/base.go +++ b/web/router/base.go @@ -189,4 +189,9 @@ func registerRouter(router *gin.Engine) { { syncRouter(syncDataRouter) } + //数据中心同步接口,不走后台登录鉴权 + statcenterGroup := router.Group("/api/web/statcenter") + { + statcenterRouter(statcenterGroup) + } } diff --git a/web/router/sync.go b/web/router/sync.go index 355770a..b29b1f6 100644 --- a/web/router/sync.go +++ b/web/router/sync.go @@ -6,6 +6,11 @@ import ( "github.com/gin-gonic/gin" ) +// statcenterRouter 数据中心拉取同步数据(协议与哔哩 /api/web/statcenter/sync 一致) +func statcenterRouter(router *gin.RouterGroup) { + router.POST("/sync", statcenterctl.StatcenterSyncList) +} + // syncRouter 数据同步 func syncRouter(router *gin.RouterGroup) { routerGroup := router.Group("/router")