书籍列表接口新增参数
This commit is contained in:
273
internal/logic/ad_event_logs/ad_event_logs.go
Normal file
273
internal/logic/ad_event_logs/ad_event_logs.go
Normal file
@ -0,0 +1,273 @@
|
||||
package ads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"server/internal/consts"
|
||||
"server/internal/dao"
|
||||
"server/internal/model"
|
||||
"server/internal/model/do"
|
||||
"server/internal/service"
|
||||
"server/utility/ecode"
|
||||
"server/utility/encrypt"
|
||||
"server/utility/mqtt"
|
||||
"server/utility/state_machine"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sAds struct {
|
||||
stateMachine *state_machine.AdStateMachine
|
||||
}
|
||||
|
||||
func New() service.IAds {
|
||||
return &sAds{
|
||||
stateMachine: state_machine.NewAdStateMachine(),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAds(New())
|
||||
}
|
||||
|
||||
// Upload 广告数据上传 - 处理用户广告状态流转
|
||||
func (s *sAds) Upload(ctx context.Context, in *model.AdsUploadIn) (out *model.AdsUploadOut, err error) {
|
||||
// 解密广告数据
|
||||
adsData, err := encrypt.DecryptAdsData(in.Data)
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("data_decrypt_failed")
|
||||
}
|
||||
|
||||
// 验证数据
|
||||
if adsData.AdsPlatId <= 0 || adsData.AdsCategoryId <= 0 || adsData.AppPackage == "" {
|
||||
return nil, ecode.Params.Sub("invalid_ads_data")
|
||||
}
|
||||
|
||||
// 使用事务同时操作多个表
|
||||
err = dao.AdEventLogs.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// 1. 查找用户该广告的最新记录,确保状态流转是在同一条广告记录上进行的
|
||||
var fromState consts.AdState
|
||||
var adEventId int64 // 广告事件ID,用于关联状态流转记录
|
||||
var isNewLifecycle bool // 是否是新的生命周期
|
||||
|
||||
// 当前状态
|
||||
toState := consts.AdState(adsData.Status)
|
||||
|
||||
// 拉取成功和拉取失败总是新的广告生命周期的开始
|
||||
if toState == consts.StateFetchSuccess || toState == consts.StateFetchFailed {
|
||||
isNewLifecycle = true
|
||||
fromState = 0
|
||||
glog.Infof(ctx, "Starting new ad lifecycle with fetch state for user %d, app %s, state: %s",
|
||||
in.UserId, adsData.AppPackage, consts.GetStateDescription(toState))
|
||||
} else {
|
||||
// 查询条件:同一用户、同一平台、同一广告类型、同一APP包名
|
||||
latestLog, err := dao.AdEventLogs.Ctx(ctx).TX(tx).
|
||||
Where(do.AdEventLogs{
|
||||
UserId: in.UserId,
|
||||
AdsPlatId: adsData.AdsPlatId,
|
||||
AdsCategoryId: adsData.AdsCategoryId,
|
||||
AppPackage: adsData.AppPackage,
|
||||
}).
|
||||
Order("created_at DESC").
|
||||
One()
|
||||
|
||||
if err == nil && !latestLog.IsEmpty() {
|
||||
fromState = consts.AdState(latestLog["status"].Int())
|
||||
adEventId = latestLog["id"].Int64()
|
||||
|
||||
// 检查是否是终止状态,如果是终止状态,则需要创建新的广告记录
|
||||
if s.stateMachine.IsTerminalState(fromState) {
|
||||
// 如果是终止状态,则表示这是一个新的广告生命周期
|
||||
isNewLifecycle = true
|
||||
fromState = 0 // 重置状态,表示新的生命周期开始
|
||||
glog.Infof(ctx, "Starting new ad lifecycle after terminal state for user %d, app %s",
|
||||
in.UserId, adsData.AppPackage)
|
||||
} else {
|
||||
glog.Infof(ctx, "Continuing ad lifecycle for user %d, app %s, from state %d to %d",
|
||||
in.UserId, adsData.AppPackage, fromState, adsData.Status)
|
||||
}
|
||||
} else {
|
||||
// 没有找到记录,这是第一次上传该广告数据
|
||||
isNewLifecycle = true
|
||||
glog.Infof(ctx, "First ad upload for user %d, app %s", in.UserId, adsData.AppPackage)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 使用状态机验证状态转换
|
||||
flowID := generateFlowID(in.UserId, adsData.AppPackage)
|
||||
|
||||
// 如果不是新的生命周期,则验证状态转换
|
||||
if !isNewLifecycle {
|
||||
err = s.stateMachine.Transition(ctx, flowID, in.UserId, fromState, toState, "用户操作")
|
||||
if err != nil {
|
||||
glog.Warningf(ctx, "Invalid state transition: %s -> %s for user %d, app %s",
|
||||
consts.GetStateDescription(fromState), consts.GetStateDescription(toState),
|
||||
in.UserId, adsData.AppPackage)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 插入或更新广告事件日志
|
||||
var eventId int64
|
||||
if isNewLifecycle {
|
||||
// 新的生命周期,创建新记录
|
||||
eventId, err = dao.AdEventLogs.Ctx(ctx).TX(tx).Data(do.AdEventLogs{
|
||||
UserId: in.UserId,
|
||||
AdsPlatId: adsData.AdsPlatId,
|
||||
AdsCategoryId: adsData.AdsCategoryId,
|
||||
AppPackage: adsData.AppPackage,
|
||||
Status: adsData.Status,
|
||||
StatusDesc: consts.GetStateDescription(toState),
|
||||
}).InsertAndGetId()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("database_save_failed")
|
||||
}
|
||||
} else {
|
||||
// 继续现有生命周期,更新状态
|
||||
_, err = dao.AdEventLogs.Ctx(ctx).TX(tx).
|
||||
Where("id", adEventId).
|
||||
Data(do.AdEventLogs{
|
||||
Status: adsData.Status,
|
||||
StatusDesc: consts.GetStateDescription(toState),
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("database_update_failed")
|
||||
}
|
||||
eventId = adEventId
|
||||
}
|
||||
|
||||
// 4. 插入状态流转记录
|
||||
var fromStatus interface{}
|
||||
if fromState != 0 {
|
||||
fromStatus = int(fromState)
|
||||
}
|
||||
|
||||
_, err = dao.AdEventTransitions.Ctx(ctx).TX(tx).Data(do.AdEventTransitions{
|
||||
EventId: eventId,
|
||||
FromStatus: fromStatus,
|
||||
ToStatus: adsData.Status,
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("transition_save_failed")
|
||||
}
|
||||
|
||||
// 5. 如果状态是观看完成(StateWatched),则给用户增加积分
|
||||
if toState == consts.StateWatched {
|
||||
// 查询任务表中状态为1(启用)且任务类型为2(广告)的任务
|
||||
taskInfo, err := dao.Tasks.Ctx(ctx).TX(tx).
|
||||
Fields("id, reward_points").
|
||||
Where("task_type", 2).
|
||||
Where("status", 1).
|
||||
One()
|
||||
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "Failed to query ad task: %v", err)
|
||||
return ecode.Fail.Sub("task_query_failed")
|
||||
}
|
||||
|
||||
if !taskInfo.IsEmpty() {
|
||||
taskId := taskInfo["id"].Int64()
|
||||
rewardPoints := taskInfo["reward_points"].Uint()
|
||||
|
||||
if rewardPoints > 0 {
|
||||
// 更新用户积分
|
||||
_, err = dao.Users.Ctx(ctx).TX(tx).
|
||||
Where("id", in.UserId).
|
||||
Increment("points", rewardPoints)
|
||||
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "Failed to update user points: %v", err)
|
||||
return ecode.Fail.Sub("user_points_update_failed")
|
||||
}
|
||||
|
||||
// 记录积分日志
|
||||
_, err = dao.UserPointsLogs.Ctx(ctx).TX(tx).Data(do.UserPointsLogs{
|
||||
UserId: in.UserId,
|
||||
ChangeType: 2, // 2=收入(earn)
|
||||
PointsChange: int(rewardPoints),
|
||||
RelatedOrderId: eventId,
|
||||
Description: "观看广告奖励",
|
||||
}).Insert()
|
||||
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "Failed to create user points log: %v", err)
|
||||
return ecode.Fail.Sub("user_points_log_create_failed")
|
||||
}
|
||||
|
||||
// 记录任务日志
|
||||
_, err = dao.TaskLogs.Ctx(ctx).TX(tx).Data(do.TaskLogs{
|
||||
TaskId: taskId,
|
||||
UserId: in.UserId,
|
||||
RewardPoints: rewardPoints,
|
||||
}).Insert()
|
||||
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "Failed to create task log: %v", err)
|
||||
return ecode.Fail.Sub("task_log_create_failed")
|
||||
}
|
||||
|
||||
glog.Infof(ctx, "User %d earned %d points for watching ad", in.UserId, rewardPoints)
|
||||
}
|
||||
} else {
|
||||
glog.Warningf(ctx, "No active ad task found")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建MQTT消息
|
||||
mqttMessage := map[string]interface{}{
|
||||
"type": "ads",
|
||||
"node_uid": in.NodeUid,
|
||||
"device_code": in.DeviceCode,
|
||||
"data": map[string]interface{}{
|
||||
"ads_plat_id": adsData.AdsPlatId,
|
||||
"ads_category_id": adsData.AdsCategoryId,
|
||||
"app_package": adsData.AppPackage,
|
||||
"status": adsData.Status,
|
||||
"timestamp": time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
// 发送到MQTT
|
||||
s.publishToMQTT(ctx, mqttMessage)
|
||||
|
||||
return &model.AdsUploadOut{Success: true}, nil
|
||||
}
|
||||
|
||||
// generateFlowID 生成流转ID
|
||||
func generateFlowID(userID int64, appPackage string) string {
|
||||
return fmt.Sprintf("flow_%d_%s_%d", userID, appPackage, time.Now().Unix())
|
||||
}
|
||||
|
||||
// publishToMQTT 发送消息到MQTT
|
||||
func (s *sAds) publishToMQTT(ctx context.Context, message map[string]interface{}) {
|
||||
mqttClient := mqtt.GetMQTTClient("amazon_sqs")
|
||||
if mqttClient == nil {
|
||||
glog.Errorf(ctx, "MQTT client not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
queueName := g.Cfg().MustGet(ctx, "sqs.ads").String()
|
||||
if queueName == "" {
|
||||
glog.Errorf(ctx, "MQTT queue name not configured")
|
||||
return
|
||||
}
|
||||
|
||||
err := mqttClient.Publish(ctx, queueName, message)
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "Failed to publish message to MQTT: %v", err)
|
||||
} else {
|
||||
glog.Infof(ctx, "Message published to MQTT: %v", message)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user