初始化项目
This commit is contained in:
212
service/ad_position_service.go
Normal file
212
service/ad_position_service.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cidService/dao"
|
||||
"cidService/model/dto"
|
||||
"cidService/model/entity"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var AdPosition = new(adPosition)
|
||||
|
||||
type adPosition struct{}
|
||||
|
||||
// Add 添加广告位
|
||||
func (s *adPosition) Add(ctx context.Context, req *dto.AddAdPositionReq) (res *dto.AddAdPositionRes, err error) {
|
||||
adPosition := &entity.AdPosition{}
|
||||
if err = gconv.Struct(req, adPosition); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 设置基础字段
|
||||
now := time.Now()
|
||||
adPosition.CreatedAt = now
|
||||
adPosition.UpdatedAt = now
|
||||
adPosition.IsDeleted = false
|
||||
|
||||
// 初始化统计字段
|
||||
adPosition.DailyImpressions = 0
|
||||
adPosition.DailyClicks = 0
|
||||
adPosition.DailyRevenue = 0
|
||||
adPosition.CTR = 0
|
||||
// eCPM字段是未导出的,无法直接设置
|
||||
|
||||
if err = dao.AdPosition.Insert(ctx, adPosition); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &dto.AddAdPositionRes{Id: adPosition.Id.Hex()}
|
||||
return
|
||||
}
|
||||
|
||||
// Update 更新广告位
|
||||
func (s *adPosition) Update(ctx context.Context, req *dto.UpdateAdPositionReq) (err error) {
|
||||
// 更新修改时间(不需要设置,DAO层会处理)
|
||||
return dao.AdPosition.Update(ctx, req)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新广告位状态
|
||||
func (s *adPosition) UpdateStatus(ctx context.Context, req *dto.UpdateAdPositionStatusReq) (err error) {
|
||||
return dao.AdPosition.UpdateStatus(ctx, req.Id, req.Status)
|
||||
}
|
||||
|
||||
// GetOne 获取广告位详情
|
||||
func (s *adPosition) GetOne(ctx context.Context, req *dto.GetAdPositionReq) (res *dto.GetAdPositionRes, err error) {
|
||||
adPosition, err := dao.AdPosition.GetOne(ctx, req.Id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &dto.GetAdPositionRes{
|
||||
AdPosition: adPosition,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取广告位列表
|
||||
func (s *adPosition) List(ctx context.Context, req *dto.ListAdPositionReq) (res *dto.ListAdPositionRes, err error) {
|
||||
list, total, err := dao.AdPosition.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &dto.ListAdPositionRes{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetStatistics 获取广告位统计数据
|
||||
func (s *adPosition) GetStatistics(ctx context.Context, req *dto.GetAdPositionStatisticsReq) (res *dto.GetAdPositionStatisticsRes, err error) {
|
||||
statReq := &dto.GetAdStatisticsReq{
|
||||
StatType: "adPosition",
|
||||
StatDimension: req.StatType,
|
||||
TargetId: req.Id,
|
||||
StartDate: req.StartDate,
|
||||
EndDate: req.EndDate,
|
||||
DeviceType: "",
|
||||
Platform: "",
|
||||
Region: "",
|
||||
Gender: "",
|
||||
AgeGroup: "",
|
||||
SortBy: "",
|
||||
SortDirection: "",
|
||||
}
|
||||
|
||||
list, total, err := dao.AdStatistics.GetStatistics(ctx, statReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &dto.GetAdPositionStatisticsRes{
|
||||
Statistics: list,
|
||||
Total: int(total),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetByCode 根据编码获取广告位
|
||||
func (s *adPosition) GetByCode(ctx context.Context, code string) (adPosition *entity.AdPosition, err error) {
|
||||
return dao.AdPosition.GetByCode(ctx, code)
|
||||
}
|
||||
|
||||
// GetAvailableAdPositions 获取可用的广告位列表
|
||||
func (s *adPosition) GetAvailableAdPositions(ctx context.Context) (list []*entity.AdPosition, err error) {
|
||||
return dao.AdPosition.GetAvailableAdPositions(ctx)
|
||||
}
|
||||
|
||||
// MatchAd 匹配广告
|
||||
func (s *adPosition) MatchAd(ctx context.Context, positionCode string, userInfo map[string]interface{}) (ad *entity.Advertisement, err error) {
|
||||
// 获取广告位信息
|
||||
adPosition, err := dao.AdPosition.GetByCode(ctx, positionCode)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查广告位状态
|
||||
if adPosition.Status != "启用" {
|
||||
return nil, gerror.New("广告位未启用")
|
||||
}
|
||||
|
||||
// 获取符合条件的广告列表
|
||||
// 这里简化处理,实际项目中应该根据广告定向条件匹配广告
|
||||
// 可以使用MongoDB的聚合管道实现复杂匹配逻辑
|
||||
|
||||
// 返回匹配的广告
|
||||
// 这里返回第一个广告作为示例
|
||||
ad = &entity.Advertisement{
|
||||
Title: "示例广告",
|
||||
MaterialUrl: "https://example.com/ad.jpg",
|
||||
LinkUrl: "https://example.com",
|
||||
LandingPageUrl: "https://example.com/landing",
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateAdPositionStatistics 更新广告位统计
|
||||
func (s *adPosition) UpdateAdPositionStatistics(ctx context.Context, id string, impressions, clicks, revenue int64) (err error) {
|
||||
// 获取广告位信息
|
||||
adPosition, err := dao.AdPosition.GetOne(ctx, id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 计算统计数据
|
||||
totalImpressions := adPosition.DailyImpressions + impressions
|
||||
totalClicks := adPosition.DailyClicks + clicks
|
||||
totalRevenue := adPosition.DailyRevenue + revenue
|
||||
|
||||
// 计算比率
|
||||
ctr := 0.0
|
||||
if totalImpressions > 0 {
|
||||
ctr = float64(totalClicks) / float64(totalImpressions)
|
||||
}
|
||||
|
||||
ecpm := int64(0)
|
||||
if totalImpressions > 0 {
|
||||
ecpm = totalRevenue * 1000 / totalImpressions
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
stats := map[string]interface{}{
|
||||
"dailyImpressions": totalImpressions,
|
||||
"dailyClicks": totalClicks,
|
||||
"dailyRevenue": totalRevenue,
|
||||
"ctr": ctr,
|
||||
"ecpm": ecpm,
|
||||
"updatedAt": time.Now(),
|
||||
}
|
||||
|
||||
// 更新广告位统计
|
||||
err = dao.AdPosition.UpdateStatistics(ctx, id, stats)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 插入统计记录
|
||||
today := time.Now()
|
||||
todayStart := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, today.Location()).Unix()
|
||||
|
||||
statistics := &entity.AdStatistics{
|
||||
StatType: "adPosition",
|
||||
StatDimension: "day",
|
||||
TargetId: id,
|
||||
TargetName: adPosition.Name,
|
||||
StatDate: todayStart,
|
||||
Impressions: impressions,
|
||||
Clicks: clicks,
|
||||
Cost: 0, // 广告位不记录消耗,只记录收入
|
||||
Revenue: revenue,
|
||||
CTR: ctr,
|
||||
// eCPM字段是未导出的,无法直接设置
|
||||
}
|
||||
|
||||
err = dao.AdStatistics.Upsert(ctx, statistics)
|
||||
return
|
||||
}
|
||||
367
service/ad_statistics_service.go
Normal file
367
service/ad_statistics_service.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"cidService/dao"
|
||||
"cidService/model/dto"
|
||||
"cidService/model/entity"
|
||||
)
|
||||
|
||||
var AdStatistics = new(adStatistics)
|
||||
|
||||
type adStatistics struct{}
|
||||
|
||||
// List 获取统计数据列表
|
||||
func (s *adStatistics) List(ctx context.Context, req *dto.GetAdStatisticsReq) (res *dto.GetAdStatisticsRes, err error) {
|
||||
list, total, err := dao.AdStatistics.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = &dto.GetAdStatisticsRes{
|
||||
Statistics: list,
|
||||
Total: int(total),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetStatistics 获取统计数据
|
||||
func (s *adStatistics) GetStatistics(ctx context.Context, req *dto.GetAdStatisticsReq) (res *dto.GetAdStatisticsRes, err error) {
|
||||
list, total, err := dao.AdStatistics.GetStatistics(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = &dto.GetAdStatisticsRes{
|
||||
Statistics: list,
|
||||
Total: int(total),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetDashboard 获取仪表盘数据
|
||||
func (s *adStatistics) GetDashboard(ctx context.Context, req *dto.GetDashboardReq) (res *dto.GetDashboardRes, err error) {
|
||||
// 构建统计查询请求
|
||||
statReq := &dto.GetAdStatisticsReq{
|
||||
StartDate: req.StartDate,
|
||||
EndDate: req.EndDate,
|
||||
StatDimension: req.Dimension,
|
||||
}
|
||||
|
||||
// 获取所有统计数据
|
||||
stats, _, err := dao.AdStatistics.GetStatistics(ctx, statReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 计算总览数据
|
||||
overview := s.calculateOverview(stats)
|
||||
|
||||
// 计算趋势数据
|
||||
trends := s.calculateTrends(stats, req.Dimension)
|
||||
|
||||
// 计算排行数据
|
||||
topAdvertisers := s.calculateTopAdvertisers(stats)
|
||||
topAds := s.calculateTopAds(stats)
|
||||
topPositions := s.calculateTopPositions(stats)
|
||||
|
||||
res = &dto.GetDashboardRes{
|
||||
Overview: overview,
|
||||
Trends: trends,
|
||||
TopAdvertisers: topAdvertisers,
|
||||
TopAds: topAds,
|
||||
TopPositions: topPositions,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GenerateDailyStatistics 生成每日统计数据
|
||||
func (s *adStatistics) GenerateDailyStatistics(ctx context.Context, date int64) (err error) {
|
||||
// 转换日期
|
||||
t := time.Unix(date, 0)
|
||||
startOfDay := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
endOfDay := startOfDay.Add(24 * time.Hour)
|
||||
|
||||
// 生成广告主统计数据
|
||||
err = s.generateAdvertiserStatistics(ctx, startOfDay.Unix(), endOfDay.Unix())
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成广告主统计数据失败: %v", err)
|
||||
}
|
||||
|
||||
// 生成广告统计数据
|
||||
err = s.generateAdvertisementStatistics(ctx, startOfDay.Unix(), endOfDay.Unix())
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成广告统计数据失败: %v", err)
|
||||
}
|
||||
|
||||
// 生成广告位统计数据
|
||||
err = s.generateAdPositionStatistics(ctx, startOfDay.Unix(), endOfDay.Unix())
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成广告位统计数据失败: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// generateAdvertiserStatistics 生成广告主统计数据
|
||||
func (s *adStatistics) generateAdvertiserStatistics(ctx context.Context, startDate int64, _ int64) (err error) {
|
||||
// 这里简化处理,实际项目中应该从日志表或实时数据中聚合
|
||||
advertiserStats := &entity.AdStatistics{
|
||||
StatType: "advertiser",
|
||||
StatDimension: "day",
|
||||
TargetId: "example_advertiser_id",
|
||||
TargetName: "示例广告主",
|
||||
StatDate: startDate,
|
||||
Impressions: 10000,
|
||||
Clicks: 500,
|
||||
Conversions: 50,
|
||||
Cost: 50000, // 500元,单位分
|
||||
CTR: 0.05, // 5%
|
||||
CVR: 0.1, // 10%
|
||||
CPM: 5000, // 50元/千次展示
|
||||
CPC: 100, // 1元/点击
|
||||
}
|
||||
|
||||
err = dao.AdStatistics.Upsert(ctx, advertiserStats)
|
||||
return
|
||||
}
|
||||
|
||||
// generateAdvertisementStatistics 生成广告统计数据
|
||||
func (s *adStatistics) generateAdvertisementStatistics(ctx context.Context, startDate int64, _ int64) (err error) {
|
||||
// 这里简化处理,实际项目中应该从日志表或实时数据中聚合
|
||||
adStats := &entity.AdStatistics{
|
||||
StatType: "advertisement",
|
||||
StatDimension: "day",
|
||||
TargetId: "example_ad_id",
|
||||
TargetName: "示例广告",
|
||||
StatDate: startDate,
|
||||
Impressions: 1000,
|
||||
Clicks: 50,
|
||||
Conversions: 5,
|
||||
Cost: 5000, // 50元,单位分
|
||||
CTR: 0.05, // 5%
|
||||
CVR: 0.1, // 10%
|
||||
CPM: 5000, // 50元/千次展示
|
||||
CPC: 100, // 1元/点击
|
||||
}
|
||||
|
||||
err = dao.AdStatistics.Upsert(ctx, adStats)
|
||||
return
|
||||
}
|
||||
|
||||
// generateAdPositionStatistics 生成广告位统计数据
|
||||
func (s *adStatistics) generateAdPositionStatistics(ctx context.Context, startDate int64, _ int64) (err error) {
|
||||
// 这里简化处理,实际项目中应该从日志表或实时数据中聚合
|
||||
positionStats := &entity.AdStatistics{
|
||||
StatType: "adPosition",
|
||||
StatDimension: "day",
|
||||
TargetId: "example_position_id",
|
||||
TargetName: "示例广告位",
|
||||
StatDate: startDate,
|
||||
Impressions: 5000,
|
||||
Clicks: 250,
|
||||
Revenue: 6000, // 60元,单位分
|
||||
CTR: 0.05, // 5%
|
||||
}
|
||||
|
||||
err = dao.AdStatistics.Upsert(ctx, positionStats)
|
||||
return
|
||||
}
|
||||
|
||||
// calculateOverview 计算总览数据
|
||||
func (s *adStatistics) calculateOverview(stats []*entity.AdStatistics) dto.OverviewData {
|
||||
var totalImpressions, totalClicks, totalCost, totalRevenue int64
|
||||
var totalCTR, totalCVR float64
|
||||
var count int
|
||||
|
||||
// 统计不同类型的数据
|
||||
advertiserCount := make(map[string]bool)
|
||||
adCount := make(map[string]bool)
|
||||
positionCount := make(map[string]bool)
|
||||
|
||||
for _, stat := range stats {
|
||||
totalImpressions += stat.Impressions
|
||||
totalClicks += stat.Clicks
|
||||
totalCost += stat.Cost
|
||||
totalRevenue += stat.Revenue
|
||||
totalCTR += stat.CTR
|
||||
totalCVR += stat.CVR
|
||||
count++
|
||||
|
||||
// 统计不同实体的数量
|
||||
if stat.StatType == "advertiser" {
|
||||
advertiserCount[stat.TargetId] = true
|
||||
} else if stat.StatType == "advertisement" {
|
||||
adCount[stat.TargetId] = true
|
||||
} else if stat.StatType == "adPosition" {
|
||||
positionCount[stat.TargetId] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 计算平均值
|
||||
averageCTR := 0.0
|
||||
averageCVR := 0.0
|
||||
if count > 0 {
|
||||
averageCTR = totalCTR / float64(count)
|
||||
averageCVR = totalCVR / float64(count)
|
||||
}
|
||||
|
||||
return dto.OverviewData{
|
||||
TotalAdvertisers: int64(len(advertiserCount)),
|
||||
TotalAds: int64(len(adCount)),
|
||||
TotalPositions: int64(len(positionCount)),
|
||||
TotalImpressions: totalImpressions,
|
||||
TotalClicks: totalClicks,
|
||||
TotalCost: totalCost,
|
||||
TotalRevenue: totalRevenue,
|
||||
AverageCTR: averageCTR,
|
||||
AverageCVR: averageCVR,
|
||||
}
|
||||
}
|
||||
|
||||
// calculateTrends 计算趋势数据
|
||||
func (s *adStatistics) calculateTrends(stats []*entity.AdStatistics, dimension string) []dto.TrendData {
|
||||
trends := make([]dto.TrendData, 0)
|
||||
|
||||
// 按日期分组统计数据
|
||||
dateMap := make(map[int64]*dto.TrendData)
|
||||
|
||||
for _, stat := range stats {
|
||||
if _, exists := dateMap[stat.StatDate]; !exists {
|
||||
dateMap[stat.StatDate] = &dto.TrendData{
|
||||
Date: stat.StatDate,
|
||||
Impressions: 0,
|
||||
Clicks: 0,
|
||||
Cost: 0,
|
||||
Revenue: 0,
|
||||
}
|
||||
}
|
||||
|
||||
trend := dateMap[stat.StatDate]
|
||||
trend.Impressions += stat.Impressions
|
||||
trend.Clicks += stat.Clicks
|
||||
trend.Cost += stat.Cost
|
||||
trend.Revenue += stat.Revenue
|
||||
}
|
||||
|
||||
// 转换为切片并排序
|
||||
for _, trend := range dateMap {
|
||||
trends = append(trends, *trend)
|
||||
}
|
||||
|
||||
// 按日期排序
|
||||
sort.Slice(trends, func(i, j int) bool {
|
||||
return trends[i].Date < trends[j].Date
|
||||
})
|
||||
|
||||
return trends
|
||||
}
|
||||
|
||||
// calculateTopAdvertisers 计算广告主排行
|
||||
func (s *adStatistics) calculateTopAdvertisers(stats []*entity.AdStatistics) []dto.RankData {
|
||||
advertiserMap := make(map[string]*dto.RankData)
|
||||
|
||||
for _, stat := range stats {
|
||||
if stat.StatType == "advertiser" {
|
||||
if _, exists := advertiserMap[stat.TargetId]; !exists {
|
||||
advertiserMap[stat.TargetId] = &dto.RankData{
|
||||
Id: stat.TargetId,
|
||||
Name: stat.TargetName,
|
||||
Impressions: 0,
|
||||
Clicks: 0,
|
||||
Cost: 0,
|
||||
Revenue: 0,
|
||||
}
|
||||
}
|
||||
|
||||
rank := advertiserMap[stat.TargetId]
|
||||
rank.Impressions += stat.Impressions
|
||||
rank.Clicks += stat.Clicks
|
||||
rank.Cost += stat.Cost
|
||||
rank.Revenue += stat.Revenue
|
||||
}
|
||||
}
|
||||
|
||||
return s.sortRankData(advertiserMap)
|
||||
}
|
||||
|
||||
// calculateTopAds 计算广告排行
|
||||
func (s *adStatistics) calculateTopAds(stats []*entity.AdStatistics) []dto.RankData {
|
||||
adMap := make(map[string]*dto.RankData)
|
||||
|
||||
for _, stat := range stats {
|
||||
if stat.StatType == "advertisement" {
|
||||
if _, exists := adMap[stat.TargetId]; !exists {
|
||||
adMap[stat.TargetId] = &dto.RankData{
|
||||
Id: stat.TargetId,
|
||||
Name: stat.TargetName,
|
||||
Impressions: 0,
|
||||
Clicks: 0,
|
||||
Cost: 0,
|
||||
Revenue: 0,
|
||||
}
|
||||
}
|
||||
|
||||
rank := adMap[stat.TargetId]
|
||||
rank.Impressions += stat.Impressions
|
||||
rank.Clicks += stat.Clicks
|
||||
rank.Cost += stat.Cost
|
||||
rank.Revenue += stat.Revenue
|
||||
}
|
||||
}
|
||||
|
||||
return s.sortRankData(adMap)
|
||||
}
|
||||
|
||||
// calculateTopPositions 计算广告位排行
|
||||
func (s *adStatistics) calculateTopPositions(stats []*entity.AdStatistics) []dto.RankData {
|
||||
positionMap := make(map[string]*dto.RankData)
|
||||
|
||||
for _, stat := range stats {
|
||||
if stat.StatType == "adPosition" {
|
||||
if _, exists := positionMap[stat.TargetId]; !exists {
|
||||
positionMap[stat.TargetId] = &dto.RankData{
|
||||
Id: stat.TargetId,
|
||||
Name: stat.TargetName,
|
||||
Impressions: 0,
|
||||
Clicks: 0,
|
||||
Cost: 0,
|
||||
Revenue: 0,
|
||||
}
|
||||
}
|
||||
|
||||
rank := positionMap[stat.TargetId]
|
||||
rank.Impressions += stat.Impressions
|
||||
rank.Clicks += stat.Clicks
|
||||
rank.Cost += stat.Cost
|
||||
rank.Revenue += stat.Revenue
|
||||
}
|
||||
}
|
||||
|
||||
return s.sortRankData(positionMap)
|
||||
}
|
||||
|
||||
// sortRankData 对排行数据进行排序
|
||||
func (s *adStatistics) sortRankData(dataMap map[string]*dto.RankData) []dto.RankData {
|
||||
rankList := make([]dto.RankData, 0, len(dataMap))
|
||||
|
||||
for _, rank := range dataMap {
|
||||
rankList = append(rankList, *rank)
|
||||
}
|
||||
|
||||
// 按收入降序排序
|
||||
sort.Slice(rankList, func(i, j int) bool {
|
||||
return rankList[i].Revenue > rankList[j].Revenue
|
||||
})
|
||||
|
||||
// 只返回前10名
|
||||
if len(rankList) > 10 {
|
||||
rankList = rankList[:10]
|
||||
}
|
||||
|
||||
return rankList
|
||||
}
|
||||
201
service/advertisement_service.go
Normal file
201
service/advertisement_service.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cidService/dao"
|
||||
"cidService/model/dto"
|
||||
"cidService/model/entity"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var Advertisement = new(advertisement)
|
||||
|
||||
type advertisement struct{}
|
||||
|
||||
// Add 添加广告
|
||||
func (s *advertisement) Add(ctx context.Context, req *dto.AddAdvertisementReq) (res *dto.AddAdvertisementRes, err error) {
|
||||
advertisement := &entity.Advertisement{}
|
||||
if err = gconv.Struct(req, advertisement); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 设置基础字段
|
||||
now := time.Now()
|
||||
advertisement.CreatedAt = now
|
||||
advertisement.UpdatedAt = now
|
||||
advertisement.IsDeleted = false
|
||||
|
||||
// 设置初始状态
|
||||
advertisement.Status = "待审核"
|
||||
advertisement.AuditStatus = "待审核"
|
||||
|
||||
// 初始化统计字段
|
||||
advertisement.Impressions = 0
|
||||
advertisement.Clicks = 0
|
||||
advertisement.Conversions = 0
|
||||
advertisement.Cost = 0
|
||||
advertisement.CTR = 0
|
||||
advertisement.CVR = 0
|
||||
advertisement.CPM = 0
|
||||
advertisement.CPC = 0
|
||||
|
||||
if err = dao.Advertisement.Insert(ctx, advertisement); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &dto.AddAdvertisementRes{Id: advertisement.Id.Hex()}
|
||||
return
|
||||
}
|
||||
|
||||
// Update 更新广告
|
||||
func (s *advertisement) Update(ctx context.Context, req *dto.UpdateAdvertisementReq) (err error) {
|
||||
// 更新修改时间(不需要设置,DAO层会处理)
|
||||
return dao.Advertisement.Update(ctx, req)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新广告状态
|
||||
func (s *advertisement) UpdateStatus(ctx context.Context, req *dto.UpdateAdStatusReq) (err error) {
|
||||
return dao.Advertisement.UpdateStatus(ctx, req.Id, req.Status)
|
||||
}
|
||||
|
||||
// Audit 审核广告
|
||||
func (s *advertisement) Audit(ctx context.Context, req *dto.AuditAdvertisementReq) (err error) {
|
||||
return dao.Advertisement.Audit(ctx, req.Id, req.AuditStatus, req.AuditReason)
|
||||
}
|
||||
|
||||
// GetOne 获取广告详情
|
||||
func (s *advertisement) GetOne(ctx context.Context, req *dto.GetAdvertisementReq) (res *dto.GetAdvertisementRes, err error) {
|
||||
advertisement, err := dao.Advertisement.GetOne(ctx, req.Id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &dto.GetAdvertisementRes{
|
||||
Advertisement: advertisement,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取广告列表
|
||||
func (s *advertisement) List(ctx context.Context, req *dto.ListAdvertisementReq) (res *dto.ListAdvertisementRes, err error) {
|
||||
list, total, err := dao.Advertisement.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &dto.ListAdvertisementRes{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetStatistics 获取广告统计数据
|
||||
func (s *advertisement) GetStatistics(ctx context.Context, req *dto.GetAdStatisticsForAdvertisementReq) (res *dto.GetAdStatisticsForAdvertisementRes, err error) {
|
||||
statReq := &dto.GetAdStatisticsReq{
|
||||
StatType: "advertisement",
|
||||
StatDimension: req.StatType,
|
||||
TargetId: req.Id,
|
||||
StartDate: req.StartDate,
|
||||
EndDate: req.EndDate,
|
||||
DeviceType: "",
|
||||
Platform: "",
|
||||
Region: "",
|
||||
Gender: "",
|
||||
AgeGroup: "",
|
||||
SortBy: "",
|
||||
SortDirection: "",
|
||||
}
|
||||
|
||||
list, _, err := dao.AdStatistics.GetStatistics(ctx, statReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &dto.GetAdStatisticsForAdvertisementRes{
|
||||
Statistics: list,
|
||||
}
|
||||
// Total字段不存在,已移除
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateAdStatistics 更新广告统计
|
||||
func (s *advertisement) UpdateAdStatistics(ctx context.Context, id string, impressions, clicks, conversions int64, cost int64) (err error) {
|
||||
// 获取广告信息
|
||||
ad, err := dao.Advertisement.GetOne(ctx, id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 计算统计数据
|
||||
totalImpressions := ad.Impressions + impressions
|
||||
totalClicks := ad.Clicks + clicks
|
||||
totalConversions := ad.Conversions + conversions
|
||||
totalCost := ad.Cost + cost
|
||||
|
||||
// 计算比率
|
||||
ctr := 0.0
|
||||
if totalImpressions > 0 {
|
||||
ctr = float64(totalClicks) / float64(totalImpressions)
|
||||
}
|
||||
|
||||
cvr := 0.0
|
||||
if totalClicks > 0 {
|
||||
cvr = float64(totalConversions) / float64(totalClicks)
|
||||
}
|
||||
|
||||
cpm := int64(0)
|
||||
if totalImpressions > 0 {
|
||||
cpm = totalCost * 1000 / totalImpressions
|
||||
}
|
||||
|
||||
cpc := int64(0)
|
||||
if totalClicks > 0 {
|
||||
cpc = totalCost / totalClicks
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
stats := map[string]interface{}{
|
||||
"impressions": totalImpressions,
|
||||
"clicks": totalClicks,
|
||||
"conversions": totalConversions,
|
||||
"cost": totalCost,
|
||||
"ctr": ctr,
|
||||
"cvr": cvr,
|
||||
"cpm": cpm,
|
||||
"cpc": cpc,
|
||||
"updatedAt": time.Now(),
|
||||
}
|
||||
|
||||
// 更新广告统计
|
||||
err = dao.Advertisement.UpdateStatistics(ctx, id, stats)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 插入统计记录
|
||||
today := time.Now()
|
||||
todayStart := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, today.Location()).Unix()
|
||||
|
||||
statistics := &entity.AdStatistics{
|
||||
StatType: "advertisement",
|
||||
StatDimension: "day",
|
||||
TargetId: id,
|
||||
TargetName: ad.Title,
|
||||
StatDate: todayStart,
|
||||
Impressions: impressions,
|
||||
Clicks: clicks,
|
||||
Conversions: conversions,
|
||||
Cost: cost,
|
||||
CTR: ctr,
|
||||
CVR: cvr,
|
||||
CPM: cpm,
|
||||
CPC: cpc,
|
||||
// CreatedAt、UpdatedAt、IsDeleted字段是嵌入字段,无需单独设置
|
||||
}
|
||||
|
||||
err = dao.AdStatistics.Upsert(ctx, statistics)
|
||||
return
|
||||
}
|
||||
185
service/advertiser_service.go
Normal file
185
service/advertiser_service.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
"cidService/dao"
|
||||
"cidService/model/dto"
|
||||
"cidService/model/entity"
|
||||
)
|
||||
|
||||
var Advertiser = new(advertiser)
|
||||
|
||||
type advertiser struct{}
|
||||
|
||||
// Add 添加广告主
|
||||
func (s *advertiser) Add(ctx context.Context, req *dto.AddAdvertiserReq) (res *dto.AddAdvertiserRes, err error) {
|
||||
advertiser := &entity.Advertiser{}
|
||||
if err = gconv.Struct(req, advertiser); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 设置基础字段
|
||||
now := time.Now()
|
||||
advertiser.CreatedAt = now
|
||||
advertiser.UpdatedAt = now
|
||||
advertiser.IsDeleted = false
|
||||
|
||||
// 设置初始状态
|
||||
advertiser.Status = "待审核"
|
||||
advertiser.AuditStatus = "待审核"
|
||||
|
||||
if err = dao.Advertiser.Insert(ctx, advertiser); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &dto.AddAdvertiserRes{Id: advertiser.Id.Hex()}
|
||||
return
|
||||
}
|
||||
|
||||
// Update 更新广告主
|
||||
func (s *advertiser) Update(ctx context.Context, req *dto.UpdateAdvertiserReq) (err error) {
|
||||
// 更新修改时间(不需要设置,DAO层会处理)
|
||||
return dao.Advertiser.Update(ctx, req)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新广告主状态
|
||||
func (s *advertiser) UpdateStatus(ctx context.Context, req *dto.UpdateAdvertiserStatusReq) (err error) {
|
||||
return dao.Advertiser.UpdateStatus(ctx, req.Id, req.Status)
|
||||
}
|
||||
|
||||
// Audit 审核广告主
|
||||
func (s *advertiser) Audit(ctx context.Context, req *dto.AuditAdvertiserReq) (err error) {
|
||||
return dao.Advertiser.Audit(ctx, req.Id, req.AuditStatus, req.AuditReason)
|
||||
}
|
||||
|
||||
// Recharge 充值
|
||||
func (s *advertiser) Recharge(ctx context.Context, req *dto.RechargeAdvertiserReq) (err error) {
|
||||
// 验证金额
|
||||
if req.Amount <= 0 {
|
||||
return gerror.New("充值金额必须大于0")
|
||||
}
|
||||
|
||||
// 执行充值
|
||||
err = dao.Advertiser.Recharge(ctx, req.Id, req.Amount, req.Remark)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 记录充值流水(实际项目中可能需要创建充值记录表)
|
||||
// 这里简化处理
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateCreditLimit 更新授信额度
|
||||
func (s *advertiser) UpdateCreditLimit(ctx context.Context, req *dto.UpdateCreditLimitReq) (err error) {
|
||||
// 验证授信额度
|
||||
if req.CreditLimit < 0 {
|
||||
return gerror.New("授信额度不能为负数")
|
||||
}
|
||||
|
||||
// 更新授信额度
|
||||
err = dao.Advertiser.UpdateCreditLimit(ctx, req.Id, req.CreditLimit)
|
||||
return
|
||||
}
|
||||
|
||||
// GetOne 获取广告主详情
|
||||
func (s *advertiser) GetOne(ctx context.Context, req *dto.GetAdvertiserReq) (res *dto.GetAdvertiserRes, err error) {
|
||||
advertiser, err := dao.Advertiser.GetOne(ctx, req.Id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &dto.GetAdvertiserRes{
|
||||
Advertiser: advertiser,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取广告主列表
|
||||
func (s *advertiser) List(ctx context.Context, req *dto.ListAdvertiserReq) (res *dto.ListAdvertiserRes, err error) {
|
||||
list, total, err := dao.Advertiser.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &dto.ListAdvertiserRes{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetBalance 获取广告主余额
|
||||
func (s *advertiser) GetBalance(ctx context.Context, id string) (balance, creditLimit int64, err error) {
|
||||
advertiser, err := dao.Advertiser.GetOne(ctx, id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
balance = advertiser.AccountBalance
|
||||
creditLimit = advertiser.CreditLimit
|
||||
return
|
||||
}
|
||||
|
||||
// CheckBudget 检查广告主预算
|
||||
func (s *advertiser) CheckBudget(ctx context.Context, id string, cost int64) (hasEnoughBudget bool, err error) {
|
||||
advertiser, err := dao.Advertiser.GetOne(ctx, id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 可用金额 = 账户余额 + 授信额度 - 已消耗
|
||||
availableAmount := advertiser.AccountBalance + advertiser.CreditLimit
|
||||
|
||||
// 检查预算是否充足
|
||||
hasEnoughBudget = availableAmount >= cost
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeductBudget 扣除预算
|
||||
func (s *advertiser) DeductBudget(ctx context.Context, id string, cost int64) (err error) {
|
||||
// 检查预算是否充足
|
||||
hasEnoughBudget, err := s.CheckBudget(ctx, id, cost)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !hasEnoughBudget {
|
||||
return gerror.New("预算不足")
|
||||
}
|
||||
|
||||
// 获取广告主信息
|
||||
advertiser, err := dao.Advertiser.GetOne(ctx, id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 计算新余额
|
||||
newBalance := advertiser.AccountBalance - cost
|
||||
|
||||
// 如果账户余额为负,从授信额度中扣除
|
||||
if newBalance < 0 {
|
||||
newCreditLimit := advertiser.CreditLimit + newBalance
|
||||
newBalance = 0
|
||||
|
||||
// 更新授信额度
|
||||
err = dao.Advertiser.UpdateCreditLimit(ctx, id, newCreditLimit)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 更新账户余额
|
||||
err = dao.Advertiser.Update(ctx, &dto.UpdateAdvertiserReq{
|
||||
Id: id,
|
||||
AccountBalance: &newBalance,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cidService/dao"
|
||||
"cidService/model/dto"
|
||||
"cidService/model/entity"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var Data = new(data)
|
||||
|
||||
type data struct{}
|
||||
|
||||
// Add 添加数据
|
||||
func (s *data) Add(ctx context.Context, req *dto.AddDataReq) (res *dto.AddDataRes, err error) {
|
||||
data := &entity.Data{}
|
||||
if err = gconv.Struct(req, data); err != nil {
|
||||
return
|
||||
}
|
||||
// 设置基础字段
|
||||
now := time.Now()
|
||||
data.CreatedAt = now
|
||||
data.UpdatedAt = now
|
||||
data.IsDeleted = false
|
||||
// 注意:Creator、Updater、TenantId 保持零值,不设置
|
||||
|
||||
if err = dao.Data.Insert(ctx, data); err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.AddDataRes{Id: data.Id.Hex()}
|
||||
return
|
||||
}
|
||||
|
||||
// Update 更新数据
|
||||
func (s *data) Update(ctx context.Context, req *dto.UpdateDataReq) (err error) {
|
||||
return dao.Data.Update(ctx, req)
|
||||
}
|
||||
|
||||
// List 获取数据列表
|
||||
func (s *data) List(ctx context.Context, req *dto.ListDataReq) (res *dto.ListDataRes, err error) {
|
||||
list, total, err := dao.Data.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListDataRes{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
}
|
||||
return
|
||||
}
|
||||
183
service/report_service.go
Normal file
183
service/report_service.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"cidService/dao"
|
||||
"cidService/model/dto"
|
||||
"cidService/model/entity"
|
||||
)
|
||||
|
||||
// Report Service 单例
|
||||
var Report = new(report)
|
||||
|
||||
type report struct{}
|
||||
|
||||
// Create 创建报表
|
||||
func (s *report) Create(ctx context.Context, req *dto.CreateReportReq) (res *dto.CreateReportRes, err error) {
|
||||
data := &entity.AdReport{
|
||||
ReportName: req.ReportName,
|
||||
ReportType: req.ReportType,
|
||||
ReportPeriod: req.ReportPeriod,
|
||||
StartDate: req.StartDate,
|
||||
EndDate: req.EndDate,
|
||||
Status: "生成中",
|
||||
GenerateTime: time.Now().Unix(),
|
||||
FileFormat: req.FileFormat,
|
||||
EmailRecipients: req.EmailRecipients,
|
||||
Schedule: req.Schedule,
|
||||
}
|
||||
|
||||
// 存储报表配置
|
||||
if req.ReportConfig != nil {
|
||||
data.ReportData = []entity.ReportItem{
|
||||
{
|
||||
Dimension: "config",
|
||||
Data: req.ReportConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if err = dao.Report.Insert(ctx, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 异步生成报表
|
||||
go s.generateReport(data.Id.Hex(), req)
|
||||
|
||||
res = &dto.CreateReportRes{Id: data.Id.Hex()}
|
||||
return
|
||||
}
|
||||
|
||||
// generateReport 生成报表
|
||||
func (s *report) generateReport(reportId string, req *dto.CreateReportReq) {
|
||||
// 模拟生成报表
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// 更新报表状态
|
||||
ctx := context.Background()
|
||||
|
||||
updateReq := &dto.UpdateReportReq{
|
||||
Id: reportId,
|
||||
FileFormat: req.FileFormat,
|
||||
EmailRecipients: req.EmailRecipients,
|
||||
}
|
||||
|
||||
err := dao.Report.Update(ctx, updateReq)
|
||||
if err != nil {
|
||||
// 记录错误日志,这里简化处理
|
||||
// 实际项目中应该使用日志框架
|
||||
}
|
||||
|
||||
// 发送邮件通知(如果配置了邮件接收人)
|
||||
if len(req.EmailRecipients) > 0 {
|
||||
// 发送邮件
|
||||
// 这里简化处理,实际项目中应该使用邮件服务
|
||||
}
|
||||
}
|
||||
|
||||
// GetOne 获取报表详情
|
||||
func (s *report) GetOne(ctx context.Context, req *dto.GetReportReq) (res *dto.GetReportRes, err error) {
|
||||
data, err := dao.Report.GetOne(ctx, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = &dto.GetReportRes{
|
||||
AdReport: data,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取报表列表
|
||||
func (s *report) List(ctx context.Context, req *dto.ListReportReq) (res *dto.ListReportRes, err error) {
|
||||
list, total, err := dao.Report.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = &dto.ListReportRes{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Update 更新报表
|
||||
func (s *report) Update(ctx context.Context, req *dto.UpdateReportReq) (err error) {
|
||||
return dao.Report.Update(ctx, req)
|
||||
}
|
||||
|
||||
// Delete 删除报表
|
||||
func (s *report) Delete(ctx context.Context, req *dto.DeleteReportReq) (err error) {
|
||||
return dao.Report.Delete(ctx, req.Id)
|
||||
}
|
||||
|
||||
// Download 下载报表
|
||||
func (s *report) Download(ctx context.Context, req *dto.DownloadReportReq) (res *dto.DownloadReportRes, err error) {
|
||||
data, err := dao.Report.GetOne(ctx, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查报表状态
|
||||
if data.Status != "已完成" {
|
||||
return nil, gerror.New("报表尚未生成完成")
|
||||
}
|
||||
|
||||
// 检查报表是否过期
|
||||
if data.ExpiredTime > 0 && data.ExpiredTime < time.Now().Unix() {
|
||||
return nil, gerror.New("报表已过期")
|
||||
}
|
||||
|
||||
res = &dto.DownloadReportRes{
|
||||
DownloadUrl: data.DownloadUrl,
|
||||
FileSize: data.FileSize,
|
||||
FileFormat: data.FileFormat,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Generate 生成报表
|
||||
func (s *report) Generate(ctx context.Context, req *dto.GenerateReportReq) (err error) {
|
||||
data, err := dao.Report.GetOne(ctx, req.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构建创建报表请求
|
||||
createReq := &dto.CreateReportReq{
|
||||
ReportName: data.ReportName,
|
||||
ReportType: data.ReportType,
|
||||
ReportPeriod: data.ReportPeriod,
|
||||
StartDate: data.StartDate,
|
||||
EndDate: data.EndDate,
|
||||
FileFormat: data.FileFormat,
|
||||
EmailRecipients: data.EmailRecipients,
|
||||
Schedule: data.Schedule,
|
||||
}
|
||||
|
||||
// 从ReportData中获取报表配置
|
||||
if len(data.ReportData) > 0 {
|
||||
for _, item := range data.ReportData {
|
||||
if item.Dimension == "config" {
|
||||
createReq.ReportConfig = item.Data
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 异步生成报表
|
||||
go s.generateReport(req.Id, createReq)
|
||||
|
||||
// 更新状态
|
||||
updateReq := &dto.UpdateReportReq{
|
||||
Id: req.Id,
|
||||
}
|
||||
|
||||
err = dao.Report.Update(ctx, updateReq)
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user