书籍列表接口新增参数

This commit is contained in:
2025-08-13 15:19:42 +08:00
parent 6ccc87f2bf
commit 8afe651c64
201 changed files with 6987 additions and 1066 deletions

View 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)
}
}