init: hgmj server initial commit
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
package reportser
|
||||
|
||||
import (
|
||||
"91jd-server/common/dataReport"
|
||||
"91jd-server/common/db"
|
||||
"91jd-server/middleware/ua"
|
||||
"91jd-server/models/v/eventmod"
|
||||
"91jd-server/models/v/mediamod"
|
||||
"91jd-server/models/v/moduleconfmod"
|
||||
"91jd-server/models/v/usermod"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
//
|
||||
// 漫画事件
|
||||
//
|
||||
|
||||
// ReportComicPurchaseEvent 购买事件
|
||||
func ReportComicPurchaseEvent(t *db.MongoTool, user *usermod.User, ua ua.UA, ip string,
|
||||
media *mediamod.Media, orderId string) (err error) {
|
||||
|
||||
mediaTypeId, mediaTypeName := extractMediaModule(media)
|
||||
|
||||
event := dataReport.NewCommonField(dataReport.GenerateGlobalEventID(), dataReport.EventTypeComicPurchase, user.UID, user.DistrictCode, ua, ip)
|
||||
|
||||
event.Payload = jsonRawMessage(&dataReport.ComicPurchaseEvent{
|
||||
ComicEventPub: dataReport.ComicEventPub{
|
||||
MediaId: media.LsjId,
|
||||
ComicId: media.ID.Hex(),
|
||||
ComicTitle: media.Title,
|
||||
ComicTypeId: mediaTypeId,
|
||||
ComicTypeName: mediaTypeName,
|
||||
RecommendTraceId: "",
|
||||
},
|
||||
CoinQuantity: jsonNumber(media.Price),
|
||||
OrderId: orderId,
|
||||
PageNo: jsonNumber(0),
|
||||
})
|
||||
|
||||
b, _ := json.Marshal(event)
|
||||
// 写入数据库
|
||||
return eventmod.Insert(t, &eventmod.Event{
|
||||
EventId: event.EventID,
|
||||
EventType: event.Event,
|
||||
Data: string(b),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// ReportComicCollectOrLikeEvent 收藏或者点赞事件上报
|
||||
func ReportComicCollectOrLikeEvent(t *db.MongoTool, user *usermod.User, ua ua.UA, ip string,
|
||||
media *mediamod.Media, isCollect bool, cancel bool) (err error) {
|
||||
|
||||
mediaTypeId, mediaTypeName := extractMediaModule(media)
|
||||
|
||||
var flag int64 = 1 // 点赞/收藏
|
||||
if cancel {
|
||||
flag = 2 // 取消点赞/取消收藏
|
||||
}
|
||||
|
||||
eventKey := dataReport.EventTypeComicLike
|
||||
if isCollect {
|
||||
eventKey = dataReport.EventTypeComicCollect
|
||||
}
|
||||
|
||||
event := dataReport.NewCommonField(dataReport.GenerateGlobalEventID(), eventKey, user.UID, user.DistrictCode, ua, ip)
|
||||
|
||||
event.Payload = jsonRawMessage(&dataReport.ComicCollectOrLikeEvent{
|
||||
ComicEventPub: dataReport.ComicEventPub{
|
||||
MediaId: media.LsjId,
|
||||
ComicId: media.ID.Hex(),
|
||||
ComicTitle: media.Title,
|
||||
ComicTypeId: mediaTypeId,
|
||||
ComicTypeName: mediaTypeName,
|
||||
RecommendTraceId: "",
|
||||
},
|
||||
Flag: jsonNumber(flag),
|
||||
PageNo: jsonNumber(0),
|
||||
})
|
||||
|
||||
b, _ := json.Marshal(event)
|
||||
// 写入数据库
|
||||
return eventmod.Insert(t, &eventmod.Event{
|
||||
EventId: event.EventID,
|
||||
EventType: event.Event,
|
||||
Data: string(b),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// ReportComicCommentEvent 评论事件
|
||||
func ReportComicCommentEvent(t *db.MongoTool, user *usermod.User, ua ua.UA, ip string,
|
||||
media *mediamod.Media, content string) (err error) {
|
||||
|
||||
mediaTypeId, mediaTypeName := extractMediaModule(media)
|
||||
|
||||
event := dataReport.NewCommonField(dataReport.GenerateGlobalEventID(), dataReport.EventTypeComicComment, user.UID, user.DistrictCode, ua, ip)
|
||||
|
||||
event.Payload = jsonRawMessage(&dataReport.ComicCommentEvent{
|
||||
ComicEventPub: dataReport.ComicEventPub{
|
||||
MediaId: media.LsjId,
|
||||
ComicId: media.ID.Hex(),
|
||||
ComicTitle: media.Title,
|
||||
ComicTypeId: mediaTypeId,
|
||||
ComicTypeName: mediaTypeName,
|
||||
RecommendTraceId: "",
|
||||
},
|
||||
CommentContent: content,
|
||||
PageNo: jsonNumber(0),
|
||||
})
|
||||
|
||||
b, _ := json.Marshal(event)
|
||||
// 写入数据库
|
||||
return eventmod.Insert(t, &eventmod.Event{
|
||||
EventId: event.EventID,
|
||||
EventType: event.Event,
|
||||
Data: string(b),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// 小说事件
|
||||
//
|
||||
|
||||
// ReportNovelPurchaseEvent 购买事件
|
||||
func ReportNovelPurchaseEvent(t *db.MongoTool, user *usermod.User, ua ua.UA, ip string,
|
||||
media *mediamod.Media, orderId string) (err error) {
|
||||
|
||||
mediaTypeId, mediaTypeName := extractMediaModule(media)
|
||||
|
||||
event := dataReport.NewCommonField(dataReport.GenerateGlobalEventID(), dataReport.EventTypeNovelPurchase, user.UID, user.DistrictCode, ua, ip)
|
||||
|
||||
event.Payload = jsonRawMessage(&dataReport.NovelPurchaseEvent{
|
||||
NovelEventPub: dataReport.NovelEventPub{
|
||||
MediaId: media.LsjId,
|
||||
NovelId: media.ID.Hex(),
|
||||
NovelTitle: media.Title,
|
||||
NovelTypeId: mediaTypeId,
|
||||
NovelTypeName: mediaTypeName,
|
||||
RecommendTraceId: "",
|
||||
},
|
||||
CoinQuantity: jsonNumber(media.Price),
|
||||
OrderId: orderId,
|
||||
PageNo: jsonNumber(0),
|
||||
})
|
||||
|
||||
b, _ := json.Marshal(event)
|
||||
// 写入数据库
|
||||
return eventmod.Insert(t, &eventmod.Event{
|
||||
EventId: event.EventID,
|
||||
EventType: event.Event,
|
||||
Data: string(b),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// ReportNovelCollectOrLikeEvent 收藏或者点赞事件上报
|
||||
func ReportNovelCollectOrLikeEvent(t *db.MongoTool, user *usermod.User, ua ua.UA, ip string,
|
||||
media *mediamod.Media, isCollect bool, cancel bool) (err error) {
|
||||
|
||||
mediaTypeId, mediaTypeName := extractMediaModule(media)
|
||||
|
||||
var flag int64 = 1 // 点赞/收藏
|
||||
if cancel {
|
||||
flag = 2 // 取消点赞/取消收藏
|
||||
}
|
||||
|
||||
eventKey := dataReport.EventTypeNovelLike //点赞
|
||||
if isCollect {
|
||||
eventKey = dataReport.EventTypeNovelCollect // 收藏
|
||||
}
|
||||
|
||||
event := dataReport.NewCommonField(dataReport.GenerateGlobalEventID(), eventKey, user.UID, user.DistrictCode, ua, ip)
|
||||
|
||||
event.Payload = jsonRawMessage(&dataReport.NovelCollectOrLikeEvent{
|
||||
NovelEventPub: dataReport.NovelEventPub{
|
||||
MediaId: media.LsjId,
|
||||
NovelId: media.ID.Hex(),
|
||||
NovelTitle: media.Title,
|
||||
NovelTypeId: mediaTypeId,
|
||||
NovelTypeName: mediaTypeName,
|
||||
RecommendTraceId: "",
|
||||
},
|
||||
Flag: jsonNumber(flag),
|
||||
PageNo: jsonNumber(0),
|
||||
})
|
||||
|
||||
b, _ := json.Marshal(event)
|
||||
// 写入数据库
|
||||
return eventmod.Insert(t, &eventmod.Event{
|
||||
EventId: event.EventID,
|
||||
EventType: event.Event,
|
||||
Data: string(b),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// ReportNovelCommentEvent 评论事件
|
||||
func ReportNovelCommentEvent(t *db.MongoTool, user *usermod.User, ua ua.UA, ip string,
|
||||
media *mediamod.Media, content string) (err error) {
|
||||
|
||||
mediaTypeId, mediaTypeName := extractMediaModule(media)
|
||||
|
||||
event := dataReport.NewCommonField(dataReport.GenerateGlobalEventID(), dataReport.EventTypeNovelComment, user.UID, user.DistrictCode, ua, ip)
|
||||
|
||||
event.Payload = jsonRawMessage(&dataReport.NovelCommentEvent{
|
||||
NovelEventPub: dataReport.NovelEventPub{
|
||||
MediaId: media.LsjId,
|
||||
NovelId: media.ID.Hex(),
|
||||
NovelTitle: media.Title,
|
||||
NovelTypeId: mediaTypeId,
|
||||
NovelTypeName: mediaTypeName,
|
||||
RecommendTraceId: "",
|
||||
},
|
||||
CommentContent: content,
|
||||
PageNo: jsonNumber(0),
|
||||
})
|
||||
|
||||
b, _ := json.Marshal(event)
|
||||
// 写入数据库
|
||||
return eventmod.Insert(t, &eventmod.Event{
|
||||
EventId: event.EventID,
|
||||
EventType: event.Event,
|
||||
Data: string(b),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// extractMediaModule 通过media的MID获取模块ID和名称
|
||||
func extractMediaModule(media *mediamod.Media) (typeID, typeName string) {
|
||||
if media == nil {
|
||||
return
|
||||
}
|
||||
if !media.MID.IsZero() {
|
||||
module, _ := moduleconfmod.GetByID(media.MID)
|
||||
if !module.ID.IsZero() {
|
||||
typeID = module.ID.Hex()
|
||||
typeName = module.ModuleName
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
typeID = "default"
|
||||
typeName = "默认分类"
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package reportser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"91jd-server/common/dataReport"
|
||||
"91jd-server/common/db"
|
||||
"91jd-server/models/v/eventmod"
|
||||
)
|
||||
|
||||
func persistEvent(t *db.MongoTool, event dataReport.CommonField) error {
|
||||
b, _ := json.Marshal(event)
|
||||
return eventmod.Insert(t, &eventmod.Event{
|
||||
EventId: event.EventID,
|
||||
EventType: event.Event,
|
||||
Data: string(b),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package reportser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91jd-server/common/dataReport"
|
||||
"91jd-server/common/db"
|
||||
"91jd-server/middleware/ua"
|
||||
"91jd-server/models/v/usermod"
|
||||
)
|
||||
|
||||
const (
|
||||
LoginTypeDeviceID = "deviceid"
|
||||
LoginTypePhone = "phone"
|
||||
LoginTypeEmail = "email"
|
||||
LoginTypeAccount = "username"
|
||||
)
|
||||
|
||||
func ReportUserLogin(t *db.MongoTool, user *usermod.User, ua ua.UA, ip string, loginType string) error {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
if loginType == "" {
|
||||
loginType = LoginTypeDeviceID
|
||||
}
|
||||
createTime := time.Now().Unix()
|
||||
eventID := newEventId(dataReport.ReportAppId(), user.UID, "", createTime)
|
||||
event := dataReport.NewCommonField(eventID, dataReport.EventTypeUserLogin, user.UID, user.DistrictCode, ua, ip)
|
||||
event.Payload = jsonRawMessage(&dataReport.UserLoginEvent{
|
||||
Type: loginType,
|
||||
})
|
||||
return persistEvent(t, event)
|
||||
}
|
||||
|
||||
func LoginTypeEventValue(loginType int) string {
|
||||
switch loginType {
|
||||
case usermod.MobileLogin:
|
||||
return LoginTypePhone
|
||||
case usermod.DevIDLogin, usermod.TokenLogin, usermod.QrCodeLogin:
|
||||
return LoginTypeDeviceID
|
||||
default:
|
||||
return LoginTypeDeviceID
|
||||
}
|
||||
}
|
||||
|
||||
func LoginTypeFromH5(p usermod.LoginWithH5) string {
|
||||
switch {
|
||||
case strings.TrimSpace(p.Mobile) != "":
|
||||
return LoginTypePhone
|
||||
case strings.TrimSpace(p.Email) != "":
|
||||
return LoginTypeEmail
|
||||
case strings.TrimSpace(p.Account) != "":
|
||||
return LoginTypeAccount
|
||||
default:
|
||||
return LoginTypeDeviceID
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package reportser
|
||||
|
||||
import (
|
||||
"91jd-server/common/dataReport"
|
||||
"91jd-server/common/db"
|
||||
"91jd-server/middleware/ua"
|
||||
"91jd-server/models/v/eventmod"
|
||||
"91jd-server/models/v/rchgordmod"
|
||||
"91jd-server/models/v/usermod"
|
||||
"91jd-server/models/v/vidmod"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ReportOrderCreatedEvent 订单创建事件
|
||||
func ReportOrderCreatedEvent(t *db.MongoTool, user *usermod.User, ua ua.UA, ip string, order *rchgordmod.RechargeOrder) (err error) {
|
||||
orderType := "coin_purchase"
|
||||
page := KeyCoinPage
|
||||
coin := order.Amount
|
||||
if order.Category == 1 {
|
||||
orderType = "vip_subscription"
|
||||
page = KeyVipPage
|
||||
coin = 0
|
||||
}
|
||||
eventId := newEventId(dataReport.ReportAppId(), user.UID, order.ID.Hex(), order.CreatedAt.Unix())
|
||||
event := dataReport.NewCommonField(eventId, dataReport.EventTypeOrderCreated, user.UID, user.DistrictCode, ua, ip)
|
||||
|
||||
var vipDurationID string
|
||||
var vipDurationName string
|
||||
if order.Category == 1 {
|
||||
vipDurationID = order.ProductID.Hex()
|
||||
vipDurationName = order.ProductName
|
||||
}
|
||||
event.Payload = jsonRawMessage(&dataReport.OrderCreatedEvent{
|
||||
OrderID: order.ID.Hex(), // 订单号(唯一标识)
|
||||
OrderType: orderType, // 订单类型
|
||||
ProductID: order.ProductID.Hex(), // 商品ID
|
||||
ProductName: order.ProductName, // 商品名称
|
||||
Amount: jsonNumber(order.Money), // 订单金额(分)
|
||||
Currency: "CNY", // 货币类型
|
||||
CoinQuantity: jsonNumber(coin), // 金币数量
|
||||
VIPDurationID: vipDurationID,
|
||||
VIPDurationName: vipDurationName,
|
||||
SourcePageKey: page.Key(), // 来源页面标识
|
||||
SourcePageName: page.Name(), // 来源页面名称
|
||||
CreateTime: jsonNumber(order.CreatedAt.Unix()),
|
||||
})
|
||||
b, _ := json.Marshal(event)
|
||||
// 写入数据库
|
||||
return eventmod.Insert(t, &eventmod.Event{
|
||||
EventId: event.EventID,
|
||||
EventType: event.Event,
|
||||
Data: string(b),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// ReportOrderPaidEvent 订单支付事件
|
||||
func ReportOrderPaidEvent(t *db.MongoTool, order rchgordmod.RechargeOrder, transactionID string, amount int64, coin int64, vipExpirationTime, successAt time.Time) (err error) {
|
||||
user, err := usermod.FindUserByUID(order.UID)
|
||||
if err != nil || user == nil {
|
||||
return
|
||||
}
|
||||
orderType := "coin_purchase"
|
||||
var vipExpirationUnix int64
|
||||
if order.Category == 1 {
|
||||
orderType = "vip_subscription"
|
||||
coin = 0
|
||||
if vipExpirationTime.Unix() > 0 {
|
||||
vipExpirationUnix = vipExpirationTime.Unix()
|
||||
}
|
||||
}
|
||||
eventId := newEventId(dataReport.ReportAppId(), user.UID, order.ID.Hex(), successAt.Unix())
|
||||
event := dataReport.NewCommonField(eventId, dataReport.EventTypeOrderPaid, user.UID, user.DistrictCode, order.Ua, order.UserIP)
|
||||
event.Payload = jsonRawMessage(&dataReport.OrderPaidEvent{
|
||||
OrderID: order.ID.Hex(), // 订单号(关联创建事件)
|
||||
OrderType: orderType, // 订单类型:coin_purchase, vip_subscription
|
||||
ProductID: order.ProductID.Hex(), // 商品ID
|
||||
Amount: jsonNumber(amount), // 实际支付金额(分)
|
||||
Currency: "CNY", // 货币类型:CNY, USD
|
||||
CoinQuantity: jsonNumber(coin), // 金币数量(仅金币订单)
|
||||
VIPExpirationTime: jsonNumber(vipExpirationUnix), // VIP过期时间(10位时间戳)
|
||||
PayType: order.RechargeType, // 支付方式:wechat, alipay, bank_card
|
||||
PayChannel: "", // 支付渠道:银行名称
|
||||
TransactionID: transactionID, // 第三方支付交易号
|
||||
CreateTime: jsonNumber(successAt.Unix()),
|
||||
})
|
||||
b, _ := json.Marshal(event)
|
||||
// 写入数据库
|
||||
return eventmod.Insert(t, &eventmod.Event{
|
||||
EventId: event.EventID,
|
||||
EventType: event.Event,
|
||||
Data: string(b),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// ReportCoinConsumeEvent 金币消耗事件
|
||||
func ReportCoinConsumeEvent(t *db.MongoTool, orderId string, orderCreatedAt time.Time, user *usermod.User, ua ua.UA, ip string, productId, productName string, amount, balanceBefore, balanceAfter int64, reason Key) (err error) {
|
||||
if amount == 0 {
|
||||
return
|
||||
}
|
||||
if productId == "" {
|
||||
if productName == "" {
|
||||
productName = "金币消耗"
|
||||
}
|
||||
productId = Md5String(productName)
|
||||
}
|
||||
|
||||
eventId := newEventId(dataReport.ReportAppId(), user.UID, orderId, orderCreatedAt.Unix())
|
||||
event := dataReport.NewCommonField(eventId, dataReport.EventTypeCoinConsume, user.UID, user.DistrictCode, ua, ip)
|
||||
event.Payload = jsonRawMessage(&dataReport.CoinConsumeEvent{
|
||||
ProductID: productId, // 商品ID
|
||||
ProductName: productName, // 商品名称
|
||||
CoinConsumeAmount: jsonNumber(amount), // 金币消耗数量
|
||||
CoinBalanceBefore: jsonNumber(balanceBefore), // 消耗前金币余额
|
||||
CoinBalanceAfter: jsonNumber(balanceAfter), // 消耗后金币余额
|
||||
ConsumeReasonKey: reason.Key(), // 消耗原因
|
||||
ConsumeReasonName: reason.Name(), // 消耗原因名称
|
||||
CreateTime: jsonNumber(orderCreatedAt.Unix()),
|
||||
OrderId: orderId,
|
||||
})
|
||||
b, _ := json.Marshal(event)
|
||||
// 写入数据库
|
||||
return eventmod.Insert(t, &eventmod.Event{
|
||||
EventId: event.EventID,
|
||||
EventType: event.Event,
|
||||
Data: string(b),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// ReportVideoPurchase 视频购买事件
|
||||
func ReportVideoPurchase(t *db.MongoTool, user *usermod.User, ua ua.UA, ip string, video *vidmod.VideoModel, orderId string) (err error) {
|
||||
meta := extractVideoMeta(video)
|
||||
event := dataReport.NewCommonField(dataReport.GenerateGlobalEventID(), dataReport.EventTypeVideoPurchase, user.UID, user.DistrictCode, ua, ip)
|
||||
event.Payload = jsonRawMessage(&dataReport.VideoPurchaseEvent{
|
||||
VideoEventCore: meta.toEventCore(video.ID.Hex(), video.Title, ""),
|
||||
CoinQuantity: jsonNumber(video.Coins), // 金币数量
|
||||
OrderID: orderId, // 订单号
|
||||
})
|
||||
b, _ := json.Marshal(event)
|
||||
// 写入数据库
|
||||
return eventmod.Insert(t, &eventmod.Event{
|
||||
EventId: event.EventID,
|
||||
EventType: event.Event,
|
||||
Data: string(b),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// ReportUserRegister 用户注册事件
|
||||
func ReportUserRegister(t *db.MongoTool, user *usermod.User, ua ua.UA, ip string) (err error) {
|
||||
registerType := "deviceid"
|
||||
eventId := newEventId(dataReport.ReportAppId(), user.UID, "", user.CreatedAt.Unix())
|
||||
event := dataReport.NewCommonField(eventId, dataReport.EventTypeUserRegister, user.UID, user.DistrictCode, ua, ip)
|
||||
event.Payload = jsonRawMessage(&dataReport.UserRegisterEvent{
|
||||
Type: registerType,
|
||||
TraceID: user.Tid,
|
||||
CreateTime: jsonNumber(user.CreatedAt.Unix()),
|
||||
})
|
||||
b, _ := json.Marshal(event)
|
||||
// 写入数据库
|
||||
return eventmod.Insert(t, &eventmod.Event{
|
||||
EventId: event.EventID,
|
||||
EventType: event.Event,
|
||||
Data: string(b),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func newEventId(appId string, uid uint64, orderId string, createTime int64) string {
|
||||
input := fmt.Sprintf("%v%v%v%v", appId, uid, orderId, createTime)
|
||||
return Md5String(input)
|
||||
}
|
||||
|
||||
func jsonNumber(v int64) json.Number {
|
||||
return json.Number(strconv.FormatInt(v, 10))
|
||||
}
|
||||
|
||||
func jsonRawMessage(v interface{}) json.RawMessage {
|
||||
b, _ := json.Marshal(v)
|
||||
return b
|
||||
}
|
||||
|
||||
func Md5String[T string | []byte](input T) string {
|
||||
h := md5.New()
|
||||
h.Write([]byte(input))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package reportser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"91jd-server/app/appg"
|
||||
"91jd-server/common/log"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
sdkConfigPath = "/sdk/getAvailableDomainList"
|
||||
sdkConfigCacheKey = "report:sdk:domain_config"
|
||||
sdkConfigCacheTTL = 30 * time.Minute
|
||||
sdkConfigFetchInterval = "0 */5 * * * *"
|
||||
)
|
||||
|
||||
var (
|
||||
sdkConfigCron *cron.Cron
|
||||
sdkConfigOnce sync.Once
|
||||
|
||||
sdkConfigMu sync.RWMutex
|
||||
sdkDomainConfig string
|
||||
sdkConfigUpdatedAt time.Time
|
||||
)
|
||||
|
||||
type sdkConfigResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// StartSDKConfigCron 定时拉取 SDK 域名配置串,服务端只缓存并原样透传。
|
||||
func StartSDKConfigCron() {
|
||||
sdkConfigOnce.Do(func() {
|
||||
if err := RefreshSDKDomainConfig(); err != nil {
|
||||
log.Error("RefreshSDKDomainConfig first run fail", log.E(err))
|
||||
}
|
||||
|
||||
sdkConfigCron = cron.New(cron.WithSeconds())
|
||||
if _, err := sdkConfigCron.AddFunc(sdkConfigFetchInterval, func() {
|
||||
if err := RefreshSDKDomainConfig(); err != nil {
|
||||
log.Error("RefreshSDKDomainConfig cron fail", log.E(err))
|
||||
}
|
||||
}); err != nil {
|
||||
log.Error("StartSDKConfigCron AddFunc fail", log.E(err))
|
||||
return
|
||||
}
|
||||
sdkConfigCron.Start()
|
||||
})
|
||||
}
|
||||
|
||||
func StopSDKConfigCron() {
|
||||
if sdkConfigCron == nil {
|
||||
return
|
||||
}
|
||||
ctx := sdkConfigCron.Stop()
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
func RefreshSDKDomainConfig() error {
|
||||
apiURL := strings.TrimRight(appg.Conf.DataReport.Url, "/") + sdkConfigPath
|
||||
data, err := fetchSDKDomainConfig(apiURL, &http.Client{Timeout: 5 * time.Second})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setSDKDomainConfig(data)
|
||||
if appg.Redis == nil {
|
||||
return nil
|
||||
}
|
||||
return appg.Redis.Set(sdkConfigCacheKey, data, sdkConfigCacheTTL)
|
||||
}
|
||||
|
||||
func GetSDKDomainConfig() string {
|
||||
sdkConfigMu.RLock()
|
||||
cached := sdkDomainConfig
|
||||
updatedAt := sdkConfigUpdatedAt
|
||||
sdkConfigMu.RUnlock()
|
||||
if cached != "" && time.Since(updatedAt) < time.Minute {
|
||||
return cached
|
||||
}
|
||||
|
||||
if appg.Redis != nil {
|
||||
data, err := appg.Redis.Get(sdkConfigCacheKey)
|
||||
if err != nil {
|
||||
log.Error("GetSDKDomainConfig redis get fail", log.E(err))
|
||||
} else if data != nil && *data != "" {
|
||||
setSDKDomainConfig(*data)
|
||||
return *data
|
||||
}
|
||||
}
|
||||
|
||||
return cached
|
||||
}
|
||||
|
||||
func setSDKDomainConfig(data string) {
|
||||
sdkConfigMu.Lock()
|
||||
defer sdkConfigMu.Unlock()
|
||||
sdkDomainConfig = data
|
||||
sdkConfigUpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
func fetchSDKDomainConfig(apiURL string, client *http.Client) (string, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("sdk config http status: %d", resp.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var result sdkConfigResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return "", fmt.Errorf("sdk config code: %d, msg: %s", result.Code, result.Msg)
|
||||
}
|
||||
if strings.TrimSpace(result.Data) == "" {
|
||||
return "", fmt.Errorf("sdk config data is empty")
|
||||
}
|
||||
return result.Data, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package reportser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFetchSDKDomainConfig(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprint(w, `{"code":0,"msg":"success","data":"domain-config"}`)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
got, err := fetchSDKDomainConfig(server.URL, server.Client())
|
||||
if err != nil {
|
||||
t.Fatalf("fetchSDKDomainConfig error = %v", err)
|
||||
}
|
||||
if got != "domain-config" {
|
||||
t.Fatalf("data = %q, want domain-config", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchSDKDomainConfigRejectsFailedResponse(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
body string
|
||||
}{
|
||||
{name: "non-zero code", body: `{"code":1,"msg":"fail","data":"domain-config"}`},
|
||||
{name: "empty data", body: `{"code":0,"msg":"success","data":""}`},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprint(w, tc.body)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if got, err := fetchSDKDomainConfig(server.URL, server.Client()); err == nil {
|
||||
t.Fatalf("fetchSDKDomainConfig = %q, want error", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package reportser
|
||||
|
||||
type Key string
|
||||
type ConsumeReasonKey = Key
|
||||
|
||||
const (
|
||||
KeyBuyVip Key = "content_purchase" // 购买会员卡
|
||||
KeyVideoUnlock Key = "video_unlock" // 视频解锁
|
||||
KeyPostUnlock Key = "content_purchase" // 帖子解锁
|
||||
KeyAcgUnlock Key = "content_purchase" // acg解锁
|
||||
KeyAcgChapterUnlock Key = "content_purchase" // acg章节解锁
|
||||
KeyGenerateAiImageChangeFace Key = "content_purchase" // 生成ai图片换脸
|
||||
KeyGenerateAiVideoChangeFace Key = "content_purchase" // 生成ai视频换脸
|
||||
KeyGenerateAiNovel Key = "content_purchase" // 生成ai小说
|
||||
KeyGenerateAiImageToVideo Key = "content_purchase" // 生成ai图生视频
|
||||
KeyGenerateAiTextToImage Key = "content_purchase" // 生成ai文生图
|
||||
KeyGenerateAiUndress Key = "content_purchase" // 生成ai脱衣
|
||||
KeyAiGirlFriendExchangeIntegral Key = "content_purchase" // ai女友兑换积分
|
||||
KeyLottery Key = "content_purchase" // 抽奖
|
||||
KeyGiftSend Key = "gift_send" // 礼物赠送
|
||||
|
||||
ConsumeReasonVideoUnlock ConsumeReasonKey = KeyVideoUnlock
|
||||
ConsumeReasonGiftSend ConsumeReasonKey = KeyGiftSend
|
||||
ConsumeReasonContentPurchase ConsumeReasonKey = KeyBuyVip
|
||||
|
||||
KeyHomePage Key = "homePage" // 首页
|
||||
KeyVipPage Key = "vipPage" // 会员卡页面
|
||||
KeyCoinPage Key = "coinPage" // 金币充值页面
|
||||
|
||||
)
|
||||
|
||||
var keyMap = map[Key]string{
|
||||
// 购买解锁相关
|
||||
KeyBuyVip: "内容购买",
|
||||
KeyVideoUnlock: "视频解锁",
|
||||
KeyGiftSend: "礼物赠送",
|
||||
// 页面相关
|
||||
KeyHomePage: "首页",
|
||||
KeyVipPage: "会员卡页面",
|
||||
KeyCoinPage: "金币充值页面",
|
||||
}
|
||||
|
||||
func (p Key) Name() string {
|
||||
return keyMap[p]
|
||||
}
|
||||
func (p Key) Key() string {
|
||||
return string(p)
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package reportser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"91jd-server/common/dataReport"
|
||||
"91jd-server/common/db"
|
||||
"91jd-server/common/log"
|
||||
"91jd-server/middleware/ua"
|
||||
"91jd-server/models/v/moduleconfmod"
|
||||
"91jd-server/models/v/usermod"
|
||||
"91jd-server/models/v/vidmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type videoMeta struct {
|
||||
mediaID string
|
||||
videoTypeID string
|
||||
videoTypeName string
|
||||
videoContentType string
|
||||
}
|
||||
|
||||
func (m videoMeta) toEventCore(videoID, title string, recommendTraceID string) dataReport.VideoEventCore {
|
||||
return dataReport.VideoEventCore{
|
||||
MediaID: m.mediaID,
|
||||
VideoID: videoID,
|
||||
VideoTitle: title,
|
||||
VideoTypeID: m.videoTypeID,
|
||||
VideoTypeName: m.videoTypeName,
|
||||
VideoContentType: m.videoContentType,
|
||||
RecommendTraceID: recommendTraceID,
|
||||
}
|
||||
}
|
||||
|
||||
func ReportVideoLikeEvent(t *db.MongoTool, user *usermod.User, ua ua.UA, ip string, video *vidmod.VideoModel, cancel bool) error {
|
||||
if user == nil || video == nil || video.ID.IsZero() {
|
||||
return nil
|
||||
}
|
||||
flag := int64(1)
|
||||
if cancel {
|
||||
flag = 2
|
||||
}
|
||||
meta := extractVideoMeta(video)
|
||||
event := dataReport.NewCommonField(dataReport.GenerateGlobalEventID(), dataReport.EventTypeVideoLike, user.UID, user.DistrictCode, ua, ip)
|
||||
event.Payload = jsonRawMessage(&dataReport.VideoLikeEvent{
|
||||
VideoEventCore: meta.toEventCore(video.ID.Hex(), video.Title, ""),
|
||||
Flag: jsonNumber(flag),
|
||||
})
|
||||
return persistEvent(t, event)
|
||||
}
|
||||
|
||||
func ReportVideoCollectEvent(t *db.MongoTool, user *usermod.User, ua ua.UA, ip string, video *vidmod.VideoModel, cancel bool) error {
|
||||
if user == nil || video == nil || video.ID.IsZero() {
|
||||
return nil
|
||||
}
|
||||
flag := int64(1)
|
||||
if cancel {
|
||||
flag = 2
|
||||
}
|
||||
meta := extractVideoMeta(video)
|
||||
event := dataReport.NewCommonField(dataReport.GenerateGlobalEventID(), dataReport.EventTypeVideoCollect, user.UID, user.DistrictCode, ua, ip)
|
||||
event.Payload = jsonRawMessage(&dataReport.VideoCollectEvent{
|
||||
VideoEventCore: meta.toEventCore(video.ID.Hex(), video.Title, ""),
|
||||
Flag: jsonNumber(flag),
|
||||
})
|
||||
return persistEvent(t, event)
|
||||
}
|
||||
|
||||
func ReportVideoCommentEvent(t *db.MongoTool, user *usermod.User, ua ua.UA, ip string, video *vidmod.VideoModel, comment string) error {
|
||||
if user == nil || video == nil || video.ID.IsZero() {
|
||||
return nil
|
||||
}
|
||||
meta := extractVideoMeta(video)
|
||||
event := dataReport.NewCommonField(dataReport.GenerateGlobalEventID(), dataReport.EventTypeVideoComment, user.UID, user.DistrictCode, ua, ip)
|
||||
event.Payload = jsonRawMessage(&dataReport.VideoCommentEvent{
|
||||
VideoEventCore: meta.toEventCore(video.ID.Hex(), video.Title, ""),
|
||||
CommentContent: comment,
|
||||
})
|
||||
return persistEvent(t, event)
|
||||
}
|
||||
|
||||
func ReportVideoStatusChangeEvent(video *vidmod.VideoModel, flag int64) {
|
||||
if video == nil || video.ID.IsZero() || flag == 0 {
|
||||
return
|
||||
}
|
||||
user, err := usermod.FindUserByUID(video.PublisherID)
|
||||
if err != nil || user == nil {
|
||||
return
|
||||
}
|
||||
meta := extractVideoMeta(video)
|
||||
uaInfo := ua.UA{SysType: "PC"}
|
||||
event := dataReport.NewCommonField(dataReport.GenerateGlobalEventID(), dataReport.EventTypeVideoStatusChange, user.UID, user.DistrictCode, uaInfo, "")
|
||||
event.Payload = jsonRawMessage(&dataReport.VideoStatusChangeEvent{
|
||||
MediaID: meta.mediaID,
|
||||
VideoID: video.ID.Hex(),
|
||||
VideoTitle: video.Title,
|
||||
VideoTypeID: meta.videoTypeID,
|
||||
VideoTypeName: meta.videoTypeName,
|
||||
Flag: jsonNumber(flag),
|
||||
})
|
||||
if err := persistEvent(nil, event); err != nil {
|
||||
log.Error("ReportVideoStatusChangeEvent fail", log.Any("video", video.ID.Hex()), log.E(err))
|
||||
}
|
||||
}
|
||||
|
||||
func ReportVideoStatusChangeByIDs(ids []primitive.ObjectID, flag int64) {
|
||||
if len(ids) == 0 || flag == 0 {
|
||||
return
|
||||
}
|
||||
videos, err := vidmod.GetVideoListByIDsNoStatus(ids)
|
||||
if err != nil {
|
||||
log.Error("ReportVideoStatusChangeByIDs fail", log.E(err))
|
||||
return
|
||||
}
|
||||
for _, video := range videos {
|
||||
if video == nil || video.ID.IsZero() {
|
||||
continue
|
||||
}
|
||||
videoCopy := *video
|
||||
ReportVideoStatusChangeEvent(&videoCopy, flag)
|
||||
}
|
||||
}
|
||||
|
||||
func ReportVideoStatusByStatus(ids []primitive.ObjectID, status int) {
|
||||
if flag := VideoStatusFlag(status); flag != 0 {
|
||||
ReportVideoStatusChangeByIDs(ids, flag)
|
||||
}
|
||||
}
|
||||
|
||||
func extractVideoMeta(video *vidmod.VideoModel) videoMeta {
|
||||
if video == nil {
|
||||
return videoMeta{}
|
||||
}
|
||||
meta := videoMeta{
|
||||
mediaID: videoMediaID(video),
|
||||
videoContentType: resolveVideoContentType(video),
|
||||
}
|
||||
if strings.TrimSpace(video.MID) != "" {
|
||||
oid, err := primitive.ObjectIDFromHex(video.MID)
|
||||
if err == nil {
|
||||
module, _ := moduleconfmod.GetByID(oid)
|
||||
if !module.ID.IsZero() {
|
||||
meta.videoTypeID = module.ID.Hex()
|
||||
meta.videoTypeName = module.ModuleName
|
||||
}
|
||||
}
|
||||
}
|
||||
if meta.videoTypeID == "" || meta.videoTypeName == "" {
|
||||
meta.videoTypeID = "default"
|
||||
meta.videoTypeName = "默认分类"
|
||||
}
|
||||
return meta
|
||||
}
|
||||
|
||||
func videoMediaID(video *vidmod.VideoModel) string {
|
||||
if video == nil {
|
||||
return ""
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(video.Via), "laosiji") {
|
||||
return strings.TrimSpace(video.SourceID)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveVideoContentType(video *vidmod.VideoModel) string {
|
||||
if video == nil {
|
||||
return ""
|
||||
}
|
||||
switch video.NewsType {
|
||||
case vidmod.SHORT:
|
||||
return "short_video"
|
||||
default:
|
||||
return "video"
|
||||
}
|
||||
}
|
||||
|
||||
func VideoStatusFlag(status int) int64 {
|
||||
switch status {
|
||||
case vidmod.OnlinePass:
|
||||
return 1
|
||||
case vidmod.OffShelf, vidmod.IsDeleted:
|
||||
return 2
|
||||
case vidmod.CheckPass, vidmod.Free:
|
||||
return 3
|
||||
case vidmod.CheckFailure:
|
||||
return 4
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user