Dockerfile
This commit is contained in:
@@ -1,167 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cid/dao"
|
||||
"cid/model/dto"
|
||||
"cid/model/entity"
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
var AdPosition = new(adPosition)
|
||||
|
||||
type adPosition struct{}
|
||||
|
||||
// Add 添加广告位
|
||||
func (s *adPosition) Add(ctx context.Context, req *dto.AddAdPositionReq) (res *dto.AddAdPositionRes, err error) {
|
||||
ids, err := dao.AdPosition.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &dto.AddAdPositionRes{Id: ids[0].(*bson.ObjectID)}
|
||||
return
|
||||
}
|
||||
|
||||
// Update 更新广告位
|
||||
func (s *adPosition) Update(ctx context.Context, req *dto.UpdateAdPositionReq) error {
|
||||
// 转换ID
|
||||
id, err := bson.ObjectIDFromHex(req.Id)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "无效的ID格式")
|
||||
}
|
||||
|
||||
// 先获取原始广告位信息
|
||||
originalAdPosition, err := dao.AdPosition.GetOne(ctx, &id)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "获取原始广告位信息失败")
|
||||
}
|
||||
|
||||
// 修改字段
|
||||
if !g.IsEmpty(req.Name) {
|
||||
originalAdPosition.Name = req.Name
|
||||
}
|
||||
if !g.IsEmpty(req.Description) {
|
||||
originalAdPosition.Description = req.Description
|
||||
}
|
||||
if !g.IsEmpty(req.PositionCode) {
|
||||
originalAdPosition.PositionCode = req.PositionCode
|
||||
}
|
||||
if !g.IsEmpty(req.AdFormat) {
|
||||
originalAdPosition.AdFormat = req.AdFormat
|
||||
}
|
||||
if req.Width != nil {
|
||||
originalAdPosition.Width = int64(*req.Width)
|
||||
}
|
||||
if req.Height != nil {
|
||||
originalAdPosition.Height = int64(*req.Height)
|
||||
}
|
||||
if !g.IsEmpty(req.Page) {
|
||||
originalAdPosition.Page = req.Page
|
||||
}
|
||||
if !g.IsEmpty(req.Section) {
|
||||
originalAdPosition.Section = req.Section
|
||||
}
|
||||
if !g.IsEmpty(req.Location) {
|
||||
originalAdPosition.Location = req.Location
|
||||
}
|
||||
if req.MaxAds != nil {
|
||||
originalAdPosition.MaxAds = *req.MaxAds
|
||||
}
|
||||
if req.RefreshInterval != nil {
|
||||
originalAdPosition.RefreshInterval = *req.RefreshInterval
|
||||
}
|
||||
if req.IsLazyLoad != nil {
|
||||
originalAdPosition.IsLazyLoad = *req.IsLazyLoad
|
||||
}
|
||||
if !g.IsEmpty(req.PricingModel) {
|
||||
originalAdPosition.PricingModel = req.PricingModel
|
||||
}
|
||||
if req.BasePrice != nil {
|
||||
originalAdPosition.BasePrice = *req.BasePrice
|
||||
}
|
||||
if req.FloorPrice != nil {
|
||||
originalAdPosition.FloorPrice = *req.FloorPrice
|
||||
}
|
||||
if !g.IsEmpty(req.PriceUnit) {
|
||||
originalAdPosition.PriceUnit = req.PriceUnit
|
||||
}
|
||||
if req.DisplayRules != nil {
|
||||
originalAdPosition.DisplayRules = req.DisplayRules
|
||||
}
|
||||
if req.Status != nil {
|
||||
originalAdPosition.Status = *req.Status
|
||||
}
|
||||
if req.IsExclusive != nil {
|
||||
originalAdPosition.IsExclusive = *req.IsExclusive
|
||||
}
|
||||
|
||||
return dao.AdPosition.Update(ctx, &id, originalAdPosition)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新广告位状态
|
||||
func (s *adPosition) UpdateStatus(ctx context.Context, req *dto.UpdateAdPositionStatusReq) error {
|
||||
id, err := bson.ObjectIDFromHex(req.Id)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "无效的ID格式")
|
||||
}
|
||||
return dao.AdPosition.UpdateStatus(ctx, &id, req.Status)
|
||||
}
|
||||
|
||||
// GetOne 获取广告位详情
|
||||
func (s *adPosition) GetOne(ctx context.Context, req *dto.GetAdPositionReq) (res *dto.GetAdPositionRes, err error) {
|
||||
id, err := bson.ObjectIDFromHex(req.Id)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "无效的ID格式")
|
||||
}
|
||||
|
||||
adPosition, err := dao.AdPosition.GetOne(ctx, &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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 返回匹配的广告
|
||||
// 这里返回第一个广告作为示例
|
||||
ad = &entity.Advertisement{
|
||||
Title: "示例广告",
|
||||
MaterialUrl: "https://example.com/ad.jpg",
|
||||
TargetUrl: "https://example.com",
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateAdPositionStatistics 更新广告位统计
|
||||
func (s *adPosition) UpdateAdPositionStatistics(ctx context.Context, id string, impressions, clicks, revenue int64) (err error) {
|
||||
return
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cid/dao"
|
||||
"cid/model/config"
|
||||
"cid/model/dto"
|
||||
"cid/model/entity"
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type adSource struct{}
|
||||
|
||||
// AdSource 广告源服务
|
||||
var AdSource = new(adSource)
|
||||
|
||||
// GetAvailableSources 获取可用的广告源列表
|
||||
func (s *adSource) GetAvailableSources(ctx context.Context) (list []*entity.AdSource, err error) {
|
||||
return dao.AdSource.GetAvailableSources(ctx)
|
||||
}
|
||||
|
||||
// GetSourcesByProvider 根据提供商获取广告源
|
||||
func (s *adSource) GetSourcesByProvider(ctx context.Context, provider string) (list []*entity.AdSource, err error) {
|
||||
return dao.AdSource.GetSourcesByProvider(ctx, provider)
|
||||
}
|
||||
|
||||
// CreateAdSource 创建广告源
|
||||
func (s *adSource) CreateAdSource(ctx context.Context, req *dto.CreateAdSourceReq) (id string, err error) {
|
||||
// 检查广告源名称是否已存在
|
||||
existingSource, err := dao.AdSource.GetByName(ctx, req.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if existingSource != nil {
|
||||
return "", gerror.New("广告源名称已存在")
|
||||
}
|
||||
|
||||
adSource := &entity.AdSource{
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Provider: req.Provider,
|
||||
Type: req.Type,
|
||||
APIConfig: config.APIConfig{
|
||||
Endpoint: req.APIEndpoint,
|
||||
},
|
||||
}
|
||||
|
||||
// 设置状态
|
||||
adSource.Status = "active" // 默认状态
|
||||
|
||||
return dao.AdSource.Create(ctx, adSource)
|
||||
}
|
||||
|
||||
// UpdateAdSource 更新广告源
|
||||
func (s *adSource) UpdateAdSource(ctx context.Context, id string, req *dto.UpdateAdSourceReq) (affected int64, err error) {
|
||||
|
||||
// 检查广告源是否存在
|
||||
existingSource, err := dao.AdSource.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if existingSource == nil {
|
||||
return 0, gerror.New("广告源不存在")
|
||||
}
|
||||
|
||||
// 如果更新名称,检查是否与其他广告源冲突
|
||||
if req.Name != "" && req.Name != existingSource.Name {
|
||||
conflictSource, err := dao.AdSource.GetByName(ctx, req.Name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if conflictSource != nil {
|
||||
return 0, gerror.New("广告源名称已存在")
|
||||
}
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
updateData := &entity.AdSource{}
|
||||
if req.Name != "" {
|
||||
updateData.Name = req.Name
|
||||
}
|
||||
if req.APIEndpoint != "" {
|
||||
updateData.APIConfig.Endpoint = req.APIEndpoint
|
||||
}
|
||||
|
||||
return dao.AdSource.UpdateFields(ctx, id, updateData)
|
||||
}
|
||||
|
||||
// DeleteAdSource 删除广告源
|
||||
func (s *adSource) DeleteAdSource(ctx context.Context, id string) (affected int64, err error) {
|
||||
// 检查广告源是否存在
|
||||
existingSource, err := dao.AdSource.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if existingSource == nil {
|
||||
return 0, gerror.New("广告源不存在")
|
||||
}
|
||||
|
||||
return dao.AdSource.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// GetAdSourceByID 根据ID获取广告源
|
||||
func (s *adSource) GetAdSourceByID(ctx context.Context, id string) (adSource *entity.AdSource, err error) {
|
||||
return dao.AdSource.GetByID(ctx, id)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cid/dao"
|
||||
"cid/model/dto"
|
||||
"cid/model/entity"
|
||||
"context"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
// 设置初始状态
|
||||
advertisement.Status = "待审核"
|
||||
|
||||
// 注意:CreatedAt、UpdatedAt、TenantId、IsDeleted等字段由common/mongo的Insert方法自动设置
|
||||
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
|
||||
}
|
||||
|
||||
// UpdateAdStatistics 更新广告统计
|
||||
func (s *advertisement) UpdateAdStatistics(ctx context.Context, id string, impressions, clicks, conversions int64, cost int64) (err error) {
|
||||
return
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
"cid/dao"
|
||||
"cid/model/dto"
|
||||
"cid/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
|
||||
}
|
||||
|
||||
// 设置初始状态
|
||||
advertiser.Status = "待审核"
|
||||
|
||||
// 注意:CreatedAt、UpdatedAt、TenantId、IsDeleted等字段由common/mongo的Insert方法自动设置
|
||||
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
|
||||
}
|
||||
178
service/app/application_service.go
Normal file
178
service/app/application_service.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
consts "cid/consts/app"
|
||||
dao "cid/dao/app"
|
||||
dto "cid/model/dto/app"
|
||||
entity "cid/model/entity/app"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type applicationService struct{}
|
||||
|
||||
// Application 应用服务
|
||||
var Application = new(applicationService)
|
||||
|
||||
// Create 创建应用
|
||||
func (s *applicationService) Create(ctx context.Context, req *dto.CreateApplicationReq) (res *dto.CreateApplicationRes, err error) {
|
||||
// 检查应用名称是否重复
|
||||
count, err := dao.Application.Count(ctx, &dto.ListApplicationReq{Name: req.Name})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, errors.New("应用名称已存在")
|
||||
}
|
||||
|
||||
// 检查应用编码是否重复
|
||||
count, err = dao.Application.Count(ctx, &dto.ListApplicationReq{AppCode: req.AppCode})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, errors.New("应用编码已存在")
|
||||
}
|
||||
|
||||
// 插入数据库
|
||||
id, err := dao.Application.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.CreateApplicationRes{
|
||||
Id: id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取应用列表
|
||||
func (s *applicationService) List(ctx context.Context, req *dto.ListApplicationReq) (res *dto.ListApplicationRes, err error) {
|
||||
applicationList, total, err := dao.Application.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 组装响应数据
|
||||
list := make([]dto.ApplicationItem, 0, len(applicationList))
|
||||
for _, item := range applicationList {
|
||||
list = append(list, dto.ApplicationItem{
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
AppCode: item.AppCode,
|
||||
Type: item.Type,
|
||||
TypeName: s.getTypeName(item.Type),
|
||||
Status: item.Status,
|
||||
StatusName: s.getStatusName(item.Status),
|
||||
Description: item.Description,
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
UpdatedAt: item.UpdatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
res = &dto.ListApplicationRes{
|
||||
List: list,
|
||||
Total: total,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetOne 获取单个应用
|
||||
func (s *applicationService) GetOne(ctx context.Context, req *dto.GetApplicationReq) (res *dto.GetApplicationRes, err error) {
|
||||
application, err := dao.Application.GetOne(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var applicationEntity *entity.Application
|
||||
if err = gconv.Struct(application, &applicationEntity); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return &dto.GetApplicationRes{
|
||||
Application: applicationEntity,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Update 更新应用
|
||||
func (s *applicationService) Update(ctx context.Context, req *dto.UpdateApplicationReq) (err error) {
|
||||
// 检查应用是否存在
|
||||
exist, err := dao.Application.GetOne(ctx, &dto.GetApplicationReq{Id: req.Id})
|
||||
if err != nil || exist == nil {
|
||||
return errors.New("应用不存在")
|
||||
}
|
||||
|
||||
// 如果修改了名称,检查新名称是否重复
|
||||
if req.Name != "" && req.Name != exist.Name {
|
||||
count, err := dao.Application.Count(ctx, &dto.ListApplicationReq{Name: req.Name})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return errors.New("应用名称已存在")
|
||||
}
|
||||
}
|
||||
|
||||
// 如果修改了应用编码,检查新编码是否重复
|
||||
if req.AppCode != "" && req.AppCode != exist.AppCode {
|
||||
count, err := dao.Application.Count(ctx, &dto.ListApplicationReq{AppCode: req.AppCode})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return errors.New("应用编码已存在")
|
||||
}
|
||||
}
|
||||
|
||||
_, err = dao.Application.Update(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateStatus 更新应用状态
|
||||
func (s *applicationService) UpdateStatus(ctx context.Context, req *dto.UpdateApplicationStatusReq) (err error) {
|
||||
_, err = dao.Application.UpdateStatus(ctx, req.Id, req.Status.String())
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除应用
|
||||
func (s *applicationService) Delete(ctx context.Context, req *dto.DeleteApplicationReq) (err error) {
|
||||
// TODO: 检查是否存在关联的数据,防止误删
|
||||
// 例如: 检查该应用是否有关联的广告活动等
|
||||
|
||||
_, err = dao.Application.Delete(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// GetByAppCode 根据应用编码获取应用
|
||||
func (s *applicationService) GetByAppCode(ctx context.Context, appCode string) (res *entity.Application, err error) {
|
||||
return dao.Application.GetByAppCode(ctx, appCode)
|
||||
}
|
||||
|
||||
// getTypeName 获取类型名称
|
||||
func (s *applicationService) getTypeName(appType consts.AppType) string {
|
||||
typeNames := map[consts.AppType]string{
|
||||
consts.AppTypeWeb: "Web应用",
|
||||
consts.AppTypeMobile: "移动应用",
|
||||
consts.AppTypeMiniApp: "小程序",
|
||||
consts.AppTypeH5: "H5应用",
|
||||
consts.AppTypeDesktop: "桌面应用",
|
||||
consts.AppTypeThirdParty: "第三方应用",
|
||||
}
|
||||
if name, ok := typeNames[appType]; ok {
|
||||
return name
|
||||
}
|
||||
return string(appType)
|
||||
}
|
||||
|
||||
// getStatusName 获取状态名称
|
||||
func (s *applicationService) getStatusName(status consts.AppStatus) string {
|
||||
statusNames := map[consts.AppStatus]string{
|
||||
consts.AppStatusActive: "启用",
|
||||
consts.AppStatusInactive: "停用",
|
||||
}
|
||||
if name, ok := statusNames[status]; ok {
|
||||
return name
|
||||
}
|
||||
return string(status)
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
|
||||
"cid/dao"
|
||||
"cid/model/dto"
|
||||
"cid/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type application struct{}
|
||||
|
||||
// Application 应用服务
|
||||
var Application = new(application)
|
||||
|
||||
// CreateApplication 创建应用
|
||||
func (s *application) CreateApplication(ctx context.Context, req *dto.CreateApplicationReq) (id string, err error) {
|
||||
// 检查应用名称是否已存在
|
||||
existingApp, err := dao.Application.GetByName(ctx, req.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if existingApp != nil {
|
||||
return "", gerror.New("应用名称已存在")
|
||||
}
|
||||
|
||||
// 生成API密钥
|
||||
appKey, appSecret, err := s.generateAPIKeys()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
application := &entity.Application{
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Description: req.Description,
|
||||
Platform: req.Platform,
|
||||
PackageName: req.PackageName,
|
||||
AppStoreURL: req.AppStoreURL,
|
||||
Categories: req.Categories,
|
||||
Tags: req.Tags,
|
||||
AdTypes: req.AdTypes,
|
||||
AppKey: appKey,
|
||||
AppSecret: appSecret,
|
||||
CallbackURL: req.CallbackURL,
|
||||
}
|
||||
|
||||
// 设置状态
|
||||
application.Status = "active"
|
||||
|
||||
return dao.Application.Create(ctx, application)
|
||||
}
|
||||
|
||||
// UpdateApplication 更新应用
|
||||
func (s *application) UpdateApplication(ctx context.Context, id string, req *dto.UpdateApplicationReq) (affected int64, err error) {
|
||||
// 检查应用是否存在
|
||||
existingApp, err := dao.Application.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if existingApp == nil {
|
||||
return 0, gerror.New("应用不存在")
|
||||
}
|
||||
|
||||
// 如果更新名称,检查是否与其他应用冲突
|
||||
if req.Name != "" && req.Name != existingApp.Name {
|
||||
conflictApp, err := dao.Application.GetByName(ctx, req.Name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if conflictApp != nil && conflictApp.Id.Hex() != id {
|
||||
return 0, gerror.New("应用名称已存在")
|
||||
}
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
updateData := &entity.Application{}
|
||||
if req.Name != "" {
|
||||
updateData.Name = req.Name
|
||||
}
|
||||
if req.Description != "" {
|
||||
updateData.Description = req.Description
|
||||
}
|
||||
if req.Platform != "" {
|
||||
updateData.Platform = req.Platform
|
||||
}
|
||||
if req.PackageName != "" {
|
||||
updateData.PackageName = req.PackageName
|
||||
}
|
||||
if req.AppStoreURL != "" {
|
||||
updateData.AppStoreURL = req.AppStoreURL
|
||||
}
|
||||
if req.CallbackURL != "" {
|
||||
updateData.CallbackURL = req.CallbackURL
|
||||
}
|
||||
if len(req.Categories) > 0 {
|
||||
updateData.Categories = req.Categories
|
||||
}
|
||||
if len(req.Tags) > 0 {
|
||||
updateData.Tags = req.Tags
|
||||
}
|
||||
if len(req.AdTypes) > 0 {
|
||||
updateData.AdTypes = req.AdTypes
|
||||
}
|
||||
|
||||
// 使用Update方法更新应用
|
||||
err = dao.Application.Update(ctx, updateData)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// GetApplicationsByTenant 获取租户下的应用列表
|
||||
func (s *application) GetApplicationsByTenant(ctx context.Context, tenantID string, platform, status string, page, size int) (list []*entity.Application, total int64, err error) {
|
||||
// 调用DAO的GetByTenantID方法获取租户下的所有应用
|
||||
apps, err := dao.Application.GetByTenantID(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 应用额外的过滤条件
|
||||
var filteredApps []*entity.Application
|
||||
for _, app := range apps {
|
||||
if platform != "" && app.Platform != platform {
|
||||
continue
|
||||
}
|
||||
if status != "" && app.Status != status {
|
||||
continue
|
||||
}
|
||||
filteredApps = append(filteredApps, app)
|
||||
}
|
||||
|
||||
// 实现简单的分页
|
||||
startIndex := (page - 1) * size
|
||||
endIndex := startIndex + size
|
||||
if startIndex >= len(filteredApps) {
|
||||
return []*entity.Application{}, int64(len(filteredApps)), nil
|
||||
}
|
||||
if endIndex > len(filteredApps) {
|
||||
endIndex = len(filteredApps)
|
||||
}
|
||||
|
||||
return filteredApps[startIndex:endIndex], int64(len(filteredApps)), nil
|
||||
}
|
||||
|
||||
// GetApplicationByKey 根据API密钥获取应用
|
||||
func (s *application) GetApplicationByKey(ctx context.Context, appKey string) (application *entity.Application, err error) {
|
||||
return dao.Application.GetByAPIKey(ctx, appKey)
|
||||
}
|
||||
|
||||
// GetApplicationByID 根据ID获取应用
|
||||
func (s *application) GetApplicationByID(ctx context.Context, id string) (application *entity.Application, err error) {
|
||||
return dao.Application.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// DeleteApplication 删除应用
|
||||
func (s *application) DeleteApplication(ctx context.Context, id string) (affected int64, err error) {
|
||||
err = dao.Application.Delete(ctx, id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// ValidateApplication 验证应用权限
|
||||
func (s *application) ValidateApplication(ctx context.Context, appKey, appSecret string) (application *entity.Application, err error) {
|
||||
app, err := dao.Application.GetByAPIKey(ctx, appKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if app == nil {
|
||||
return nil, gerror.New("应用不存在")
|
||||
}
|
||||
if app.Status != "active" {
|
||||
return nil, gerror.New("应用状态异常")
|
||||
}
|
||||
if app.AppSecret != appSecret {
|
||||
return nil, gerror.New("密钥验证失败")
|
||||
}
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
// generateAPIKeys 生成API密钥
|
||||
func (s *application) generateAPIKeys() (appKey, appSecret string, err error) {
|
||||
// 生成32位随机字符串作为AppKey
|
||||
keyBytes := make([]byte, 16)
|
||||
if _, err := rand.Read(keyBytes); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
appKey = hex.EncodeToString(keyBytes)
|
||||
|
||||
// 生成64位随机字符串作为AppSecret
|
||||
secretBytes := make([]byte, 32)
|
||||
if _, err := rand.Read(secretBytes); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
appSecret = hex.EncodeToString(secretBytes)
|
||||
|
||||
return appKey, appSecret, nil
|
||||
}
|
||||
|
||||
// ResetAPIKeys 重置API密钥
|
||||
func (s *application) ResetAPIKeys(ctx context.Context, id string) (appKey, appSecret string, err error) {
|
||||
// 检查应用是否存在
|
||||
existingApp, err := dao.Application.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if existingApp == nil {
|
||||
return "", "", gerror.New("应用不存在")
|
||||
}
|
||||
|
||||
// 生成新的API密钥
|
||||
appKey, appSecret, err = s.generateAPIKeys()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 更新应用密钥
|
||||
updateData := &entity.Application{
|
||||
AppKey: appKey,
|
||||
AppSecret: appSecret,
|
||||
}
|
||||
err = dao.Application.Update(ctx, updateData)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return appKey, appSecret, nil
|
||||
}
|
||||
@@ -1,377 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cid/dao"
|
||||
"cid/model/dto"
|
||||
"cid/model/entity"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gitea.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var (
|
||||
CID = cid{}
|
||||
)
|
||||
|
||||
type cid struct{}
|
||||
|
||||
// AdMatchingStrategy 广告匹配策略
|
||||
type AdMatchingStrategy struct {
|
||||
TenantLevel string // 租户级别
|
||||
MinConversion float64 // 最低转化率
|
||||
MaxConversion float64 // 最高转化率
|
||||
SourceWeight map[string]int // 广告源权重
|
||||
MaxAdsPerRequest int // 每次请求最大广告数
|
||||
}
|
||||
|
||||
// getMatchingStrategy 获取匹配策略
|
||||
func (s *cid) getMatchingStrategy(ctx context.Context, tenantLevel string) (*AdMatchingStrategy, error) {
|
||||
// 从数据库获取策略
|
||||
strategyEntity, err := Strategy.GetStrategyByTenantLevel(ctx, tenantLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strategyEntity == nil {
|
||||
// 返回默认策略
|
||||
return &AdMatchingStrategy{
|
||||
TenantLevel: tenantLevel,
|
||||
MinConversion: 0.01,
|
||||
MaxConversion: 0.05,
|
||||
SourceWeight: map[string]int{"self": 100},
|
||||
MaxAdsPerRequest: 3,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 反序列化权重配置
|
||||
var sourceWeights map[string]int
|
||||
if strategyEntity.SourceWeights != "" {
|
||||
err = json.Unmarshal([]byte(strategyEntity.SourceWeights), &sourceWeights)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "策略权重反序列化失败: %v", err)
|
||||
sourceWeights = map[string]int{"self": 100}
|
||||
}
|
||||
}
|
||||
|
||||
return &AdMatchingStrategy{
|
||||
TenantLevel: tenantLevel, // 使用传入的tenantLevel参数
|
||||
MinConversion: strategyEntity.MinConversion,
|
||||
MaxConversion: strategyEntity.MaxConversion,
|
||||
SourceWeight: sourceWeights,
|
||||
MaxAdsPerRequest: strategyEntity.MaxAdsPerReq,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateCID 生成CID广告
|
||||
func (s *cid) GenerateCID(ctx context.Context, req *dto.GenerateCIDReq) (res *dto.GenerateCIDRes, err error) {
|
||||
// 获取当前用户信息
|
||||
userInfo, err := utils.GetUserInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取用户信息失败")
|
||||
}
|
||||
|
||||
// 获取租户信息
|
||||
tenant, err := s.getTenantByUser(ctx, gconv.Int64(userInfo.UserName))
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取租户信息失败")
|
||||
}
|
||||
|
||||
// 检查租户请求次数限制
|
||||
// 将租户ID转换为int64用于限流检查
|
||||
tenantIdInt := int64(0)
|
||||
if tenant.Id != "" && tenant.Id != "default" {
|
||||
tryInt, _ := strconv.ParseInt(tenant.Id, 10, 64)
|
||||
tenantIdInt = tryInt
|
||||
}
|
||||
|
||||
allowed, err := RateLimit.CheckTenantRequestLimit(ctx, tenantIdInt, nil)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "检查租户请求限制失败")
|
||||
}
|
||||
if !allowed {
|
||||
return nil, gerror.New("租户请求次数已超过限制,请稍后再试")
|
||||
}
|
||||
|
||||
// 获取匹配策略
|
||||
strategy, err := s.getMatchingStrategy(ctx, tenant.Level)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取匹配策略失败")
|
||||
}
|
||||
|
||||
// 根据策略获取广告
|
||||
ads, err := s.matchAds(ctx, req, strategy)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "广告匹配失败")
|
||||
}
|
||||
|
||||
// 记录CID请求
|
||||
go s.recordCIDRequest(context.Background(), req, tenant, ads)
|
||||
|
||||
// 生成唯一CID
|
||||
cid := s.generateUniqueCID()
|
||||
|
||||
// 转换租户ID为int64(兼容性处理)
|
||||
// 这里直接使用之前已经声明的tenantIdInt变量,不需要重新声明
|
||||
if tenant.Id != "" && tenant.Id != "default" {
|
||||
// 这里简化处理,实际可能需要更复杂的转换逻辑
|
||||
tryInt, parseErr := strconv.ParseInt(tenant.Id, 10, 64)
|
||||
if parseErr == nil {
|
||||
tenantIdInt = tryInt
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.GenerateCIDRes{
|
||||
CID: cid,
|
||||
Ads: ads,
|
||||
TotalAds: len(ads),
|
||||
TenantId: tenantIdInt,
|
||||
TenantName: tenant.Name,
|
||||
GeneratedAt: time.Now().Format("2006-01-02 15:04:05"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getTenantByUser 根据用户获取租户信息
|
||||
func (s *cid) getTenantByUser(ctx context.Context, userId int64) (*dto.TenantInfo, error) {
|
||||
// 通过common模块获取用户信息,包含租户ID
|
||||
userInfo, err := utils.GetUserInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取用户信息失败")
|
||||
}
|
||||
|
||||
// 租户ID直接从用户信息中获取
|
||||
tenantId := ""
|
||||
if userInfo.TenantId != nil {
|
||||
if tenantIdStr, ok := userInfo.TenantId.(string); ok && tenantIdStr != "" {
|
||||
tenantId = tenantIdStr
|
||||
}
|
||||
} else {
|
||||
tenantId = "default" // 默认租户ID
|
||||
tenantId = "default" // 默认租户ID
|
||||
}
|
||||
|
||||
// 租户级别和名称可以根据租户ID通过其他方式获取或配置
|
||||
// 这里使用映射配置,实际项目中可能需要调用其他服务
|
||||
tenantName := "默认租户"
|
||||
tenantLevel := "basic"
|
||||
|
||||
// 根据租户ID设置不同的级别(示例逻辑)
|
||||
switch tenantId {
|
||||
case "default":
|
||||
tenantName = "基础租户"
|
||||
tenantLevel = "basic"
|
||||
case "standard":
|
||||
tenantName = "标准租户"
|
||||
tenantLevel = "standard"
|
||||
case "premium":
|
||||
tenantName = "高级租户"
|
||||
tenantLevel = "premium"
|
||||
default:
|
||||
// 如果租户ID不是预设值,使用租户ID作为名称
|
||||
tenantName = tenantId + "租户"
|
||||
tenantLevel = "basic"
|
||||
}
|
||||
|
||||
return &dto.TenantInfo{
|
||||
Id: tenantId,
|
||||
Name: tenantName,
|
||||
Level: tenantLevel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// matchAds 根据策略匹配广告
|
||||
func (s *cid) matchAds(ctx context.Context, req *dto.GenerateCIDReq, strategy *AdMatchingStrategy) ([]*dto.AdInfo, error) {
|
||||
var matchedAds []*dto.AdInfo
|
||||
|
||||
// 根据策略权重从不同源获取广告
|
||||
for source, weight := range strategy.SourceWeight {
|
||||
if weight <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
sourceAds, err := s.getAdsFromSource(ctx, source, req, strategy, weight)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "从广告源 %s 获取广告失败: %v", source, err)
|
||||
continue
|
||||
}
|
||||
|
||||
matchedAds = append(matchedAds, sourceAds...)
|
||||
}
|
||||
|
||||
// 过滤符合转化率要求的广告
|
||||
var filteredAds []*dto.AdInfo
|
||||
for _, ad := range matchedAds {
|
||||
if ad.ConversionRate >= strategy.MinConversion && ad.ConversionRate <= strategy.MaxConversion {
|
||||
filteredAds = append(filteredAds, ad)
|
||||
}
|
||||
}
|
||||
|
||||
// 限制广告数量
|
||||
if len(filteredAds) > strategy.MaxAdsPerRequest {
|
||||
rand.Shuffle(len(filteredAds), func(i, j int) {
|
||||
filteredAds[i], filteredAds[j] = filteredAds[j], filteredAds[i]
|
||||
})
|
||||
filteredAds = filteredAds[:strategy.MaxAdsPerRequest]
|
||||
}
|
||||
|
||||
return filteredAds, nil
|
||||
}
|
||||
|
||||
// getAdsFromSource 从指定广告源获取广告
|
||||
func (s *cid) getAdsFromSource(ctx context.Context, source string, req *dto.GenerateCIDReq, strategy *AdMatchingStrategy, weight int) ([]*dto.AdInfo, error) {
|
||||
switch source {
|
||||
case "self":
|
||||
return s.getSelfServiceAds(ctx, req, weight)
|
||||
case "google":
|
||||
return s.getGoogleAds(ctx, req, weight)
|
||||
case "facebook":
|
||||
return s.getFacebookAds(ctx, req, weight)
|
||||
default:
|
||||
return nil, gerror.Newf("不支持的广告源: %s", source)
|
||||
}
|
||||
}
|
||||
|
||||
// getSelfServiceAds 获取自营广告
|
||||
func (s *cid) getSelfServiceAds(ctx context.Context, req *dto.GenerateCIDReq, count int) ([]*dto.AdInfo, error) {
|
||||
// 这里应该从数据库查询自营广告
|
||||
// 暂时返回模拟数据
|
||||
ads := make([]*dto.AdInfo, 0)
|
||||
for i := 0; i < count; i++ {
|
||||
ads = append(ads, &dto.AdInfo{
|
||||
Id: int64(rand.Intn(89999) + 10000),
|
||||
Title: fmt.Sprintf("自营广告 %d", i+1),
|
||||
Description: "这是一个高质量的自营广告",
|
||||
ImageUrl: "https://example.com/ad.jpg",
|
||||
TargetUrl: "https://example.com/landing",
|
||||
ConversionRate: rand.Float64(),
|
||||
Source: "self",
|
||||
Bid: rand.Intn(901) + 100,
|
||||
})
|
||||
}
|
||||
return ads, nil
|
||||
}
|
||||
|
||||
// getGoogleAds 获取Google广告
|
||||
func (s *cid) getGoogleAds(ctx context.Context, req *dto.GenerateCIDReq, count int) ([]*dto.AdInfo, error) {
|
||||
// 这里应该调用Google Ads API
|
||||
// 暂时返回模拟数据
|
||||
ads := make([]*dto.AdInfo, 0)
|
||||
for i := 0; i < count; i++ {
|
||||
ads = append(ads, &dto.AdInfo{
|
||||
Id: int64(rand.Intn(9999) + 20000),
|
||||
Title: fmt.Sprintf("Google广告 %d", i+1),
|
||||
Description: "来自Google的高质量广告",
|
||||
ImageUrl: "https://google.com/ad.jpg",
|
||||
TargetUrl: "https://google.com/landing",
|
||||
ConversionRate: rand.Float64()*0.3 + 0.1,
|
||||
Source: "google",
|
||||
Bid: rand.Intn(1301) + 200,
|
||||
})
|
||||
}
|
||||
return ads, nil
|
||||
}
|
||||
|
||||
// getFacebookAds 获取Facebook广告
|
||||
func (s *cid) getFacebookAds(ctx context.Context, req *dto.GenerateCIDReq, count int) ([]*dto.AdInfo, error) {
|
||||
// 这里应该调用Facebook Ads API
|
||||
// 暂时返回模拟数据
|
||||
ads := make([]*dto.AdInfo, 0)
|
||||
for i := 0; i < count; i++ {
|
||||
ads = append(ads, &dto.AdInfo{
|
||||
Id: int64(rand.Intn(9999) + 30000),
|
||||
Title: fmt.Sprintf("Facebook广告 %d", i+1),
|
||||
Description: "来自Facebook的高质量广告",
|
||||
ImageUrl: "https://facebook.com/ad.jpg",
|
||||
TargetUrl: "https://facebook.com/landing",
|
||||
ConversionRate: rand.Float64()*0.25 + 0.08,
|
||||
Source: "facebook",
|
||||
Bid: rand.Intn(1051) + 150,
|
||||
})
|
||||
}
|
||||
return ads, nil
|
||||
}
|
||||
|
||||
// generateUniqueCID 生成唯一CID
|
||||
func (s *cid) generateUniqueCID() string {
|
||||
timestamp := time.Now().Unix()
|
||||
random := rand.Intn(8999) + 1000
|
||||
return fmt.Sprintf("CID_%d_%d", timestamp, random)
|
||||
}
|
||||
|
||||
// recordCIDRequest 记录CID请求
|
||||
func (s *cid) recordCIDRequest(ctx context.Context, req *dto.GenerateCIDReq, tenant *dto.TenantInfo, ads []*dto.AdInfo) {
|
||||
// 转换dto.AdInfo到entity.Ad
|
||||
var entityAds []entity.Ad
|
||||
for _, ad := range ads {
|
||||
entityAds = append(entityAds, entity.Ad{
|
||||
ID: fmt.Sprintf("%d", ad.Id),
|
||||
AdSource: ad.Source,
|
||||
Title: ad.Title,
|
||||
Description: ad.Description,
|
||||
CreativeURL: ad.ImageUrl,
|
||||
LandingURL: ad.TargetUrl,
|
||||
BidAmount: int64(ad.Bid),
|
||||
})
|
||||
}
|
||||
|
||||
request := &entity.CidRequest{
|
||||
RequestID: fmt.Sprintf("REQ_%d_%d", time.Now().Unix(), rand.Intn(10000)),
|
||||
UserID: fmt.Sprintf("%d", req.UserId),
|
||||
Response: &entity.CidResponse{
|
||||
Ads: entityAds,
|
||||
},
|
||||
ProcessingTime: int64(rand.Intn(401) + 100), // 模拟处理时间
|
||||
}
|
||||
|
||||
_, err := dao.CIDRequest.Create(ctx, request)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "记录CID请求失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCIDHistory 获取CID请求历史
|
||||
func (s *cid) GetCIDHistory(ctx context.Context, userId int64, page, size int) (res *dto.GetCIDHistoryRes, err error) {
|
||||
history, total, err := dao.CIDRequest.GetHistory(ctx, strconv.FormatInt(userId, 10), page, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var historyList []*dto.CIDRequestHistory
|
||||
for _, record := range history {
|
||||
// 解析TenantID
|
||||
tenantId := int64(0)
|
||||
if tenantIdStr, ok := record.TenantId.(string); ok && tenantIdStr != "" {
|
||||
tenantId, _ = strconv.ParseInt(tenantIdStr, 10, 64)
|
||||
}
|
||||
|
||||
// 解析UserID
|
||||
uid := int64(0)
|
||||
if record.UserID != "" {
|
||||
uid, _ = strconv.ParseInt(record.UserID, 10, 64)
|
||||
}
|
||||
|
||||
historyList = append(historyList, &dto.CIDRequestHistory{
|
||||
Id: 0, // 使用默认值,因为entity使用的是ObjectID
|
||||
TenantId: tenantId,
|
||||
UserId: uid,
|
||||
RequestType: "CID", // 默认值
|
||||
Status: "completed", // 从response状态获取
|
||||
ProcessTime: int(record.ProcessingTime),
|
||||
CreatedAt: record.CreatedAt.String(),
|
||||
})
|
||||
}
|
||||
|
||||
return &dto.GetCIDHistoryRes{
|
||||
List: historyList,
|
||||
Total: total,
|
||||
Page: page,
|
||||
Size: size,
|
||||
}, nil
|
||||
}
|
||||
191
service/data/api_interface_service.go
Normal file
191
service/data/api_interface_service.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
consts "cid/consts/data"
|
||||
dao "cid/dao/data"
|
||||
dto "cid/model/dto/data"
|
||||
entity "cid/model/entity/data"
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type apiInterfaceService struct{}
|
||||
|
||||
// ApiInterface 接口服务
|
||||
var ApiInterface = new(apiInterfaceService)
|
||||
|
||||
// Create 创建接口
|
||||
func (s *apiInterfaceService) Create(ctx context.Context, req *dto.CreateApiInterfaceReq) (res *dto.CreateApiInterfaceRes, err error) {
|
||||
// 检查平台是否存在
|
||||
_, err = dao.Platform.GetOne(ctx, &dto.GetPlatformReq{Id: req.PlatformId})
|
||||
if err != nil {
|
||||
return nil, errors.New("平台不存在")
|
||||
}
|
||||
|
||||
// 检查接口编码在同一平台下是否重复
|
||||
interfaces, _, err := dao.ApiInterface.List(ctx, &dto.ListApiInterfaceReq{
|
||||
PlatformId: req.PlatformId,
|
||||
Code: req.Code,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(interfaces) > 0 {
|
||||
return nil, errors.New("接口编码在该平台下已存在")
|
||||
}
|
||||
|
||||
// 插入数据库
|
||||
id, err := dao.ApiInterface.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.CreateApiInterfaceRes{
|
||||
Id: id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取接口列表
|
||||
func (s *apiInterfaceService) List(ctx context.Context, req *dto.ListApiInterfaceReq) (res *dto.ListApiInterfaceRes, err error) {
|
||||
apiList, total, err := dao.ApiInterface.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取平台ID列表用于批量查询
|
||||
platformIds := make([]int64, 0)
|
||||
for _, item := range apiList {
|
||||
if item.PlatformId > 0 {
|
||||
platformIds = append(platformIds, item.PlatformId)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量获取平台信息
|
||||
platformMap := make(map[int64]string)
|
||||
if len(platformIds) > 0 {
|
||||
platforms, _, err := dao.Platform.List(ctx, &dto.ListPlatformReq{})
|
||||
if err == nil {
|
||||
for _, p := range platforms {
|
||||
platformMap[p.Id] = p.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组装响应数据
|
||||
list := make([]dto.ApiInterfaceItem, 0, len(apiList))
|
||||
for _, item := range apiList {
|
||||
platformName := ""
|
||||
if name, ok := platformMap[item.PlatformId]; ok {
|
||||
platformName = name
|
||||
}
|
||||
|
||||
list = append(list, dto.ApiInterfaceItem{
|
||||
Id: item.Id,
|
||||
PlatformId: item.PlatformId,
|
||||
PlatformName: platformName,
|
||||
Name: item.Name,
|
||||
Code: item.Code,
|
||||
Url: item.Url,
|
||||
Method: item.Method,
|
||||
Status: item.Status,
|
||||
StatusName: s.getStatusName(item.Status),
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
UpdatedAt: item.UpdatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
res = &dto.ListApiInterfaceRes{
|
||||
List: list,
|
||||
Total: total,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetOne 获取单个接口
|
||||
func (s *apiInterfaceService) GetOne(ctx context.Context, req *dto.GetApiInterfaceReq) (res *dto.GetApiInterfaceRes, err error) {
|
||||
apiInterface, err := dao.ApiInterface.GetOne(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取平台名称
|
||||
var platformName string
|
||||
if apiInterface.PlatformId > 0 {
|
||||
platform, _ := dao.Platform.GetOne(ctx, &dto.GetPlatformReq{Id: apiInterface.PlatformId})
|
||||
if platform != nil {
|
||||
platformName = platform.Name
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.GetApiInterfaceRes{
|
||||
ApiInterface: apiInterface,
|
||||
PlatformName: platformName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Update 更新接口
|
||||
func (s *apiInterfaceService) Update(ctx context.Context, req *dto.UpdateApiInterfaceReq) (err error) {
|
||||
// 检查接口是否存在
|
||||
exist, err := dao.ApiInterface.GetOne(ctx, &dto.GetApiInterfaceReq{Id: req.Id})
|
||||
if err != nil || exist == nil {
|
||||
return errors.New("接口不存在")
|
||||
}
|
||||
|
||||
// 如果修改了平台,检查平台是否存在
|
||||
if req.PlatformId > 0 && req.PlatformId != exist.PlatformId {
|
||||
_, err := dao.Platform.GetOne(ctx, &dto.GetPlatformReq{Id: req.PlatformId})
|
||||
if err != nil {
|
||||
return errors.New("平台不存在")
|
||||
}
|
||||
}
|
||||
|
||||
// 如果修改了编码,检查编码是否重复
|
||||
if req.Code != "" && req.Code != exist.Code {
|
||||
platformId := req.PlatformId
|
||||
if platformId == 0 {
|
||||
platformId = exist.PlatformId
|
||||
}
|
||||
interfaces, _, err := dao.ApiInterface.List(ctx, &dto.ListApiInterfaceReq{
|
||||
PlatformId: platformId,
|
||||
Code: req.Code,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(interfaces) > 0 {
|
||||
return errors.New("接口编码在该平台下已存在")
|
||||
}
|
||||
}
|
||||
|
||||
_, err = dao.ApiInterface.Update(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateStatus 更新接口状态
|
||||
func (s *apiInterfaceService) UpdateStatus(ctx context.Context, req *dto.UpdateApiInterfaceStatusReq) (err error) {
|
||||
_, err = dao.ApiInterface.UpdateStatus(ctx, req.Id, req.Status.String())
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除接口
|
||||
func (s *apiInterfaceService) Delete(ctx context.Context, req *dto.DeleteApiInterfaceReq) (err error) {
|
||||
_, err = dao.ApiInterface.Delete(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// GetByIds 根据ID列表获取接口
|
||||
func (s *apiInterfaceService) GetByIds(ctx context.Context, ids []int64) (res []entity.ApiInterface, err error) {
|
||||
return dao.ApiInterface.GetByIds(ctx, ids)
|
||||
}
|
||||
|
||||
// getStatusName 获取状态名称
|
||||
func (s *apiInterfaceService) getStatusName(status consts.PlatformStatus) string {
|
||||
statusNames := map[consts.PlatformStatus]string{
|
||||
consts.PlatformStatusActive: "启用",
|
||||
consts.PlatformStatusInactive: "停用",
|
||||
}
|
||||
if name, ok := statusNames[status]; ok {
|
||||
return name
|
||||
}
|
||||
return string(status)
|
||||
}
|
||||
316
service/data/data_fetch_service.go
Normal file
316
service/data/data_fetch_service.go
Normal file
@@ -0,0 +1,316 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
consts "cid/consts/data"
|
||||
dao "cid/dao/data"
|
||||
dto "cid/model/dto/data"
|
||||
entity "cid/model/entity/data"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/grpool"
|
||||
"github.com/gogf/gf/v2/util/guid"
|
||||
)
|
||||
|
||||
type dataFetchService struct{}
|
||||
|
||||
// DataFetch 数据获取服务
|
||||
var DataFetch = new(dataFetchService)
|
||||
|
||||
// FetchPool 数据获取协程池,限制并发数避免goroutine爆炸
|
||||
var FetchPool = grpool.New(10)
|
||||
|
||||
// Execute 执行数据获取
|
||||
func (s *dataFetchService) Execute(ctx context.Context, req *dto.ExecuteDataFetchReq) (res *dto.ExecuteDataFetchRes, err error) {
|
||||
// 检查接口是否存在
|
||||
apiInterface, err := dao.ApiInterface.GetOne(ctx, &dto.GetApiInterfaceReq{Id: req.InterfaceId})
|
||||
if err != nil || apiInterface == nil {
|
||||
return nil, fmt.Errorf("接口不存在")
|
||||
}
|
||||
|
||||
// 检查接口状态
|
||||
if apiInterface.Status != consts.PlatformStatusActive {
|
||||
return nil, fmt.Errorf("接口未启用")
|
||||
}
|
||||
|
||||
// 检查平台是否存在
|
||||
platform, err := dao.Platform.GetOne(ctx, &dto.GetPlatformReq{Id: apiInterface.PlatformId})
|
||||
if err != nil || platform == nil {
|
||||
return nil, fmt.Errorf("平台不存在")
|
||||
}
|
||||
|
||||
// 检查平台状态
|
||||
if platform.Status != consts.PlatformStatusActive {
|
||||
return nil, fmt.Errorf("平台未启用")
|
||||
}
|
||||
|
||||
// 生成请求ID
|
||||
requestId := guid.S()
|
||||
startTime := time.Now().UnixMilli()
|
||||
|
||||
// 创建数据获取日志
|
||||
fetchLog := &entity.DataFetchLog{
|
||||
PlatformId: req.PlatformId,
|
||||
InterfaceId: req.InterfaceId,
|
||||
RequestId: requestId,
|
||||
Status: consts.FetchStatusPending,
|
||||
StartTime: startTime,
|
||||
RequestConfig: req.RequestParams,
|
||||
RetryCount: 0,
|
||||
}
|
||||
|
||||
logId, err := dao.DataFetchLog.Insert(ctx, fetchLog)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建日志失败: %v", err)
|
||||
}
|
||||
|
||||
// 使用协程池异步执行数据获取
|
||||
asyncCtx := context.WithoutCancel(ctx)
|
||||
FetchPool.Add(asyncCtx, func(ctx context.Context) {
|
||||
s.executeFetch(ctx, logId, requestId, platform, apiInterface, req.RequestParams, startTime)
|
||||
})
|
||||
|
||||
return &dto.ExecuteDataFetchRes{
|
||||
RequestId: requestId,
|
||||
Status: string(consts.FetchStatusPending),
|
||||
Message: "任务已提交",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BatchExecute 批量执行数据获取
|
||||
func (s *dataFetchService) BatchExecute(ctx context.Context, req *dto.BatchExecuteDataFetchReq) (res *dto.BatchExecuteDataFetchRes, err error) {
|
||||
var requestIds []string
|
||||
successCount := 0
|
||||
failedCount := 0
|
||||
|
||||
for _, interfaceId := range req.InterfaceIds {
|
||||
executeReq := &dto.ExecuteDataFetchReq{
|
||||
PlatformId: 0, // 接口服务会自动获取
|
||||
InterfaceId: interfaceId,
|
||||
RequestParams: req.RequestParams,
|
||||
}
|
||||
|
||||
executeRes, err := s.Execute(ctx, executeReq)
|
||||
if err != nil {
|
||||
failedCount++
|
||||
g.Log().Error(ctx, "批量执行失败", g.Map{
|
||||
"interfaceId": interfaceId,
|
||||
"error": err.Error(),
|
||||
})
|
||||
} else {
|
||||
successCount++
|
||||
requestIds = append(requestIds, executeRes.RequestId)
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.BatchExecuteDataFetchRes{
|
||||
SuccessCount: successCount,
|
||||
FailedCount: failedCount,
|
||||
RequestIds: requestIds,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// List 获取数据获取日志列表
|
||||
func (s *dataFetchService) List(ctx context.Context, req *dto.ListDataFetchLogReq) (res *dto.ListDataFetchLogRes, err error) {
|
||||
logList, total, err := dao.DataFetchLog.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取平台和接口ID列表
|
||||
platformIds := make([]int64, 0)
|
||||
interfaceIds := make([]int64, 0)
|
||||
for _, item := range logList {
|
||||
if item.PlatformId > 0 {
|
||||
platformIds = append(platformIds, item.PlatformId)
|
||||
}
|
||||
if item.InterfaceId > 0 {
|
||||
interfaceIds = append(interfaceIds, item.InterfaceId)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量获取平台和接口信息
|
||||
platformMap := make(map[int64]string)
|
||||
interfaceMap := make(map[int64]string)
|
||||
if len(platformIds) > 0 {
|
||||
platforms, _, err := dao.Platform.List(ctx, &dto.ListPlatformReq{})
|
||||
if err == nil {
|
||||
for _, p := range platforms {
|
||||
platformMap[p.Id] = p.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(interfaceIds) > 0 {
|
||||
interfaces, _, err := dao.ApiInterface.List(ctx, &dto.ListApiInterfaceReq{})
|
||||
if err == nil {
|
||||
for _, i := range interfaces {
|
||||
interfaceMap[i.Id] = i.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组装响应数据
|
||||
list := make([]dto.DataFetchLogItem, 0, len(logList))
|
||||
for _, item := range logList {
|
||||
platformName := ""
|
||||
if name, ok := platformMap[item.PlatformId]; ok {
|
||||
platformName = name
|
||||
}
|
||||
interfaceName := ""
|
||||
if name, ok := interfaceMap[item.InterfaceId]; ok {
|
||||
interfaceName = name
|
||||
}
|
||||
|
||||
list = append(list, dto.DataFetchLogItem{
|
||||
Id: item.Id,
|
||||
PlatformId: item.PlatformId,
|
||||
PlatformName: platformName,
|
||||
InterfaceId: item.InterfaceId,
|
||||
InterfaceName: interfaceName,
|
||||
RequestId: item.RequestId,
|
||||
Status: item.Status,
|
||||
StatusName: s.getStatusName(item.Status),
|
||||
StartTime: item.StartTime,
|
||||
EndTime: item.EndTime,
|
||||
Duration: item.Duration,
|
||||
ErrorMessage: item.ErrorMessage,
|
||||
RetryCount: item.RetryCount,
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
res = &dto.ListDataFetchLogRes{
|
||||
List: list,
|
||||
Total: total,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetOne 获取单个数据获取日志
|
||||
func (s *dataFetchService) GetOne(ctx context.Context, req *dto.GetDataFetchLogReq) (res *dto.GetDataFetchLogRes, err error) {
|
||||
fetchLog, err := dao.DataFetchLog.GetOne(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取平台和接口名称
|
||||
var platformName, interfaceName string
|
||||
if fetchLog.PlatformId > 0 {
|
||||
platform, _ := dao.Platform.GetOne(ctx, &dto.GetPlatformReq{Id: fetchLog.PlatformId})
|
||||
if platform != nil {
|
||||
platformName = platform.Name
|
||||
}
|
||||
}
|
||||
if fetchLog.InterfaceId > 0 {
|
||||
apiInterface, _ := dao.ApiInterface.GetOne(ctx, &dto.GetApiInterfaceReq{Id: fetchLog.InterfaceId})
|
||||
if apiInterface != nil {
|
||||
interfaceName = apiInterface.Name
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.GetDataFetchLogRes{
|
||||
DataFetchLog: fetchLog,
|
||||
PlatformName: platformName,
|
||||
InterfaceName: interfaceName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReExecute 重新执行数据获取
|
||||
func (s *dataFetchService) ReExecute(ctx context.Context, req *dto.ReExecuteDataFetchReq) (res *dto.ReExecuteDataFetchRes, err error) {
|
||||
// 获取原日志
|
||||
oldLog, err := dao.DataFetchLog.GetOne(ctx, &dto.GetDataFetchLogReq{Id: req.LogId})
|
||||
if err != nil || oldLog == nil {
|
||||
return nil, fmt.Errorf("日志不存在")
|
||||
}
|
||||
|
||||
// 检查接口是否存在
|
||||
apiInterface, err := dao.ApiInterface.GetOne(ctx, &dto.GetApiInterfaceReq{Id: oldLog.InterfaceId})
|
||||
if err != nil || apiInterface == nil {
|
||||
return nil, fmt.Errorf("接口不存在")
|
||||
}
|
||||
|
||||
// 检查平台是否存在
|
||||
platform, err := dao.Platform.GetOne(ctx, &dto.GetPlatformReq{Id: apiInterface.PlatformId})
|
||||
if err != nil || platform == nil {
|
||||
return nil, fmt.Errorf("平台不存在")
|
||||
}
|
||||
|
||||
// 生成新的请求ID
|
||||
requestId := guid.S()
|
||||
startTime := time.Now().UnixMilli()
|
||||
|
||||
// 创建新的数据获取日志
|
||||
fetchLog := &entity.DataFetchLog{
|
||||
PlatformId: oldLog.PlatformId,
|
||||
InterfaceId: oldLog.InterfaceId,
|
||||
RequestId: requestId,
|
||||
Status: consts.FetchStatusPending,
|
||||
StartTime: startTime,
|
||||
RequestConfig: oldLog.RequestConfig,
|
||||
RetryCount: oldLog.RetryCount + 1,
|
||||
}
|
||||
|
||||
logId, err := dao.DataFetchLog.Insert(ctx, fetchLog)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建日志失败: %v", err)
|
||||
}
|
||||
|
||||
// 使用协程池异步执行数据获取
|
||||
asyncCtx := context.WithoutCancel(ctx)
|
||||
FetchPool.Add(asyncCtx, func(ctx context.Context) {
|
||||
s.executeFetch(ctx, logId, requestId, platform, apiInterface, oldLog.RequestConfig, startTime)
|
||||
})
|
||||
|
||||
return &dto.ReExecuteDataFetchRes{
|
||||
RequestId: requestId,
|
||||
Status: string(consts.FetchStatusPending),
|
||||
Message: "任务已重新提交",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// executeFetch 执行实际的数据获取(在协程池中运行)
|
||||
func (s *dataFetchService) executeFetch(ctx context.Context, logId int64, requestId string, platform *entity.Platform, apiInterface *entity.ApiInterface, requestParams map[string]interface{}, startTime int64) {
|
||||
// 更新状态为执行中
|
||||
dao.DataFetchLog.UpdateStatus(ctx, logId, string(consts.FetchStatusRunning), 0, 0, "", "")
|
||||
|
||||
// TODO: 根据平台和接口配置执行HTTP请求
|
||||
// 这里需要根据平台的限流配置进行限流控制
|
||||
// 根据接口的请求配置组装请求参数
|
||||
// 调用接口获取数据
|
||||
|
||||
// 模拟接口调用
|
||||
time.Sleep(time.Second * 2)
|
||||
|
||||
// 模拟成功响应
|
||||
responseData := `{"code": 0, "message": "success", "data": {"items": []}}`
|
||||
endTime := time.Now().UnixMilli()
|
||||
duration := int(endTime - startTime)
|
||||
|
||||
// 更新状态为成功
|
||||
dao.DataFetchLog.UpdateStatus(ctx, logId, string(consts.FetchStatusSuccess), endTime, duration, responseData, "")
|
||||
|
||||
g.Log().Info(ctx, "数据获取成功", g.Map{
|
||||
"logId": logId,
|
||||
"requestId": requestId,
|
||||
"platform": platform.Name,
|
||||
"interface": apiInterface.Name,
|
||||
"duration": duration,
|
||||
})
|
||||
}
|
||||
|
||||
// getStatusName 获取状态名称
|
||||
func (s *dataFetchService) getStatusName(status consts.FetchStatus) string {
|
||||
statusNames := map[consts.FetchStatus]string{
|
||||
consts.FetchStatusPending: "待执行",
|
||||
consts.FetchStatusRunning: "执行中",
|
||||
consts.FetchStatusSuccess: "成功",
|
||||
consts.FetchStatusFailed: "失败",
|
||||
consts.FetchStatusRateLimit: "触发限流",
|
||||
}
|
||||
if name, ok := statusNames[status]; ok {
|
||||
return name
|
||||
}
|
||||
return string(status)
|
||||
}
|
||||
189
service/data/platform_service.go
Normal file
189
service/data/platform_service.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
consts "cid/consts/data"
|
||||
dao "cid/dao/data"
|
||||
dto "cid/model/dto/data"
|
||||
entity "cid/model/entity/data"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type platformService struct{}
|
||||
|
||||
// Platform 平台服务
|
||||
var Platform = new(platformService)
|
||||
|
||||
// Create 创建平台
|
||||
func (s *platformService) Create(ctx context.Context, req *dto.CreatePlatformReq) (res *dto.CreatePlatformRes, err error) {
|
||||
// 检查平台名称是否重复
|
||||
count, err := dao.Platform.Count(ctx, &dto.ListPlatformReq{Name: req.Name})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, errors.New("平台名称已存在")
|
||||
}
|
||||
|
||||
// 检查平台类型是否重复
|
||||
count, err = dao.Platform.Count(ctx, &dto.ListPlatformReq{Type: req.Type})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, errors.New("该类型平台已存在")
|
||||
}
|
||||
|
||||
// 插入数据库
|
||||
id, err := dao.Platform.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.CreatePlatformRes{
|
||||
Id: id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取平台列表
|
||||
func (s *platformService) List(ctx context.Context, req *dto.ListPlatformReq) (res *dto.ListPlatformRes, err error) {
|
||||
platformList, total, err := dao.Platform.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 组装响应数据
|
||||
list := make([]dto.PlatformItem, 0, len(platformList))
|
||||
for _, item := range platformList {
|
||||
list = append(list, dto.PlatformItem{
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
Type: item.Type,
|
||||
TypeName: s.getTypeName(item.Type),
|
||||
Status: item.Status,
|
||||
StatusName: s.getStatusName(item.Status),
|
||||
Description: item.Description,
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
UpdatedAt: item.UpdatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
res = &dto.ListPlatformRes{
|
||||
List: list,
|
||||
Total: total,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetOne 获取单个平台
|
||||
func (s *platformService) GetOne(ctx context.Context, req *dto.GetPlatformReq) (res *dto.GetPlatformRes, err error) {
|
||||
platform, err := dao.Platform.GetOne(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var platformEntity *entity.Platform
|
||||
if err = gconv.Struct(platform, &platformEntity); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return &dto.GetPlatformRes{
|
||||
Platform: platformEntity,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Update 更新平台
|
||||
func (s *platformService) Update(ctx context.Context, req *dto.UpdatePlatformReq) (err error) {
|
||||
// 检查平台是否存在
|
||||
exist, err := dao.Platform.GetOne(ctx, &dto.GetPlatformReq{Id: req.Id})
|
||||
if err != nil || exist == nil {
|
||||
return errors.New("平台不存在")
|
||||
}
|
||||
|
||||
// 如果修改了名称,检查新名称是否重复
|
||||
if req.Name != "" && req.Name != exist.Name {
|
||||
count, err := dao.Platform.Count(ctx, &dto.ListPlatformReq{Name: req.Name})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return errors.New("平台名称已存在")
|
||||
}
|
||||
}
|
||||
|
||||
// 如果修改了类型,检查新类型是否重复
|
||||
if req.Type != "" && req.Type != exist.Type {
|
||||
count, err := dao.Platform.Count(ctx, &dto.ListPlatformReq{Type: req.Type})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return errors.New("该类型平台已存在")
|
||||
}
|
||||
}
|
||||
|
||||
_, err = dao.Platform.Update(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateStatus 更新平台状态
|
||||
func (s *platformService) UpdateStatus(ctx context.Context, req *dto.UpdatePlatformStatusReq) (err error) {
|
||||
_, err = dao.Platform.UpdateStatus(ctx, req.Id, req.Status.String())
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除平台
|
||||
func (s *platformService) Delete(ctx context.Context, req *dto.DeletePlatformReq) (err error) {
|
||||
// 检查是否存在关联的接口
|
||||
interfaces, _, err := dao.ApiInterface.List(ctx, &dto.ListApiInterfaceReq{
|
||||
PlatformId: req.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(interfaces) > 0 {
|
||||
return errors.New("该平台下存在接口,无法删除")
|
||||
}
|
||||
|
||||
_, err = dao.Platform.Delete(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// GetByType 根据类型获取平台
|
||||
func (s *platformService) GetByType(ctx context.Context, platformType consts.SyncPlatform) (res *entity.Platform, err error) {
|
||||
return dao.Platform.GetByType(ctx, platformType.String())
|
||||
}
|
||||
|
||||
// getTypeName 获取类型名称
|
||||
func (s *platformService) getTypeName(platformType consts.SyncPlatform) string {
|
||||
typeNames := map[consts.SyncPlatform]string{
|
||||
consts.PlatformTaobao: "淘宝",
|
||||
consts.PlatformJD: "京东",
|
||||
consts.PlatformKuaishou: "快手",
|
||||
consts.PlatformDouyin: "抖音",
|
||||
consts.PlatformXhs: "小红书",
|
||||
consts.PlatformPdd: "拼多多",
|
||||
consts.PlatformXianyu: "闲鱼",
|
||||
consts.PlatformTmall: "天猫",
|
||||
consts.PlatformWechat: "微信",
|
||||
consts.PlatformCustom: "自定义",
|
||||
}
|
||||
if name, ok := typeNames[platformType]; ok {
|
||||
return name
|
||||
}
|
||||
return string(platformType)
|
||||
}
|
||||
|
||||
// getStatusName 获取状态名称
|
||||
func (s *platformService) getStatusName(status consts.PlatformStatus) string {
|
||||
statusNames := map[consts.PlatformStatus]string{
|
||||
consts.PlatformStatusActive: "启用",
|
||||
consts.PlatformStatusInactive: "停用",
|
||||
}
|
||||
if name, ok := statusNames[status]; ok {
|
||||
return name
|
||||
}
|
||||
return string(status)
|
||||
}
|
||||
306
service/mapping/data_mapping_service.go
Normal file
306
service/mapping/data_mapping_service.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package mapping
|
||||
|
||||
import (
|
||||
consts "cid/consts/mapping"
|
||||
dao "cid/dao/mapping"
|
||||
dto "cid/model/dto/mapping"
|
||||
entity "cid/model/entity/mapping"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type dataMappingService struct{}
|
||||
|
||||
// DataMapping 数据映射服务
|
||||
var DataMapping = new(dataMappingService)
|
||||
|
||||
// Create 创建数据映射
|
||||
func (s *dataMappingService) Create(ctx context.Context, req *dto.CreateDataMappingReq) (res *dto.CreateDataMappingRes, err error) {
|
||||
// 检查接口是否存在
|
||||
// TODO: 这里需要调用data层的ApiInterface服务,暂时跳过
|
||||
|
||||
// 检查同一接口下目标字段是否重复
|
||||
existing, err := dao.DataMapping.GetByInterfaceIdAndTargetField(ctx, req.InterfaceId, req.TargetField)
|
||||
if err == nil && existing != nil {
|
||||
return nil, errors.New("该接口下目标字段已存在")
|
||||
}
|
||||
|
||||
// 插入数据库
|
||||
id, err := dao.DataMapping.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.CreateDataMappingRes{
|
||||
Id: id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// BatchCreate 批量创建数据映射
|
||||
func (s *dataMappingService) BatchCreate(ctx context.Context, req *dto.BatchCreateDataMappingReq) (res *dto.BatchCreateDataMappingRes, err error) {
|
||||
var ids []int64
|
||||
successCount := 0
|
||||
failedCount := 0
|
||||
|
||||
for _, mappingReq := range req.Mappings {
|
||||
// 设置平台和接口ID
|
||||
mappingReq.PlatformId = req.PlatformId
|
||||
mappingReq.InterfaceId = req.InterfaceId
|
||||
|
||||
createRes, err := s.Create(ctx, &mappingReq)
|
||||
if err != nil {
|
||||
failedCount++
|
||||
g.Log().Error(ctx, "批量创建映射失败", g.Map{
|
||||
"sourceField": mappingReq.SourceField,
|
||||
"targetField": mappingReq.TargetField,
|
||||
"error": err.Error(),
|
||||
})
|
||||
} else {
|
||||
successCount++
|
||||
ids = append(ids, createRes.Id)
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.BatchCreateDataMappingRes{
|
||||
SuccessCount: successCount,
|
||||
FailedCount: failedCount,
|
||||
Ids: ids,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// List 获取数据映射列表
|
||||
func (s *dataMappingService) List(ctx context.Context, req *dto.ListDataMappingReq) (res *dto.ListDataMappingRes, err error) {
|
||||
mappingList, total, err := dao.DataMapping.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取平台和接口ID列表
|
||||
platformIds := make([]int64, 0)
|
||||
interfaceIds := make([]int64, 0)
|
||||
for _, item := range mappingList {
|
||||
if item.PlatformId > 0 {
|
||||
platformIds = append(platformIds, item.PlatformId)
|
||||
}
|
||||
if item.InterfaceId > 0 {
|
||||
interfaceIds = append(interfaceIds, item.InterfaceId)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 批量获取平台和接口信息(需要调用data层服务)
|
||||
platformMap := make(map[int64]string)
|
||||
interfaceMap := make(map[int64]string)
|
||||
|
||||
// 组装响应数据
|
||||
list := make([]dto.DataMappingItem, 0, len(mappingList))
|
||||
for _, item := range mappingList {
|
||||
platformName := ""
|
||||
if name, ok := platformMap[item.PlatformId]; ok {
|
||||
platformName = name
|
||||
}
|
||||
interfaceName := ""
|
||||
if name, ok := interfaceMap[item.InterfaceId]; ok {
|
||||
interfaceName = name
|
||||
}
|
||||
|
||||
list = append(list, dto.DataMappingItem{
|
||||
Id: item.Id,
|
||||
PlatformId: item.PlatformId,
|
||||
PlatformName: platformName,
|
||||
InterfaceId: item.InterfaceId,
|
||||
InterfaceName: interfaceName,
|
||||
SourceField: item.SourceField,
|
||||
TargetField: item.TargetField,
|
||||
FieldType: item.FieldType,
|
||||
DefaultValue: item.DefaultValue,
|
||||
TransformRule: item.TransformRule,
|
||||
Priority: item.Priority,
|
||||
Status: item.Status,
|
||||
StatusName: s.getStatusName(item.Status),
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
UpdatedAt: item.UpdatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
res = &dto.ListDataMappingRes{
|
||||
List: list,
|
||||
Total: total,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetOne 获取单个数据映射
|
||||
func (s *dataMappingService) GetOne(ctx context.Context, req *dto.GetDataMappingReq) (res *dto.GetDataMappingRes, err error) {
|
||||
mapping, err := dao.DataMapping.GetOne(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 获取平台和接口名称
|
||||
platformName := ""
|
||||
interfaceName := ""
|
||||
|
||||
return &dto.GetDataMappingRes{
|
||||
DataMapping: mapping,
|
||||
PlatformName: platformName,
|
||||
InterfaceName: interfaceName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Update 更新数据映射
|
||||
func (s *dataMappingService) Update(ctx context.Context, req *dto.UpdateDataMappingReq) (err error) {
|
||||
// 检查映射是否存在
|
||||
exist, err := dao.DataMapping.GetOne(ctx, &dto.GetDataMappingReq{Id: req.Id})
|
||||
if err != nil || exist == nil {
|
||||
return errors.New("映射不存在")
|
||||
}
|
||||
|
||||
// 如果修改了目标字段,检查是否重复
|
||||
if req.TargetField != "" && req.TargetField != exist.TargetField {
|
||||
interfaceId := req.InterfaceId
|
||||
if interfaceId == 0 {
|
||||
interfaceId = exist.InterfaceId
|
||||
}
|
||||
existing, err := dao.DataMapping.GetByInterfaceIdAndTargetField(ctx, interfaceId, req.TargetField)
|
||||
if err == nil && existing != nil && existing.Id != req.Id {
|
||||
return errors.New("该接口下目标字段已存在")
|
||||
}
|
||||
}
|
||||
|
||||
_, err = dao.DataMapping.Update(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除数据映射
|
||||
func (s *dataMappingService) Delete(ctx context.Context, req *dto.DeleteDataMappingReq) (err error) {
|
||||
_, err = dao.DataMapping.Delete(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Execute 执行数据映射
|
||||
func (s *dataMappingService) Execute(ctx context.Context, req *dto.ExecuteDataMappingReq) (res *dto.ExecuteDataMappingRes, err error) {
|
||||
// 获取接口的所有映射规则
|
||||
mappings, err := dao.DataMapping.GetByInterfaceId(ctx, req.InterfaceId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(mappings) == 0 {
|
||||
return &dto.ExecuteDataMappingRes{
|
||||
TargetData: map[string]interface{}{},
|
||||
AppliedRules: []string{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 初始化目标数据
|
||||
targetData := make(map[string]interface{})
|
||||
appliedRules := make([]string, 0)
|
||||
|
||||
// 遍历映射规则进行转换
|
||||
for _, mapping := range mappings {
|
||||
if mapping.Status != consts.MappingStatusActive {
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取源数据值
|
||||
sourceValue, exists := req.SourceData[mapping.SourceField]
|
||||
|
||||
// 应用转换规则
|
||||
targetValue := s.applyTransformRule(ctx, sourceValue, exists, mapping)
|
||||
|
||||
// 设置目标数据
|
||||
targetData[mapping.TargetField] = targetValue
|
||||
|
||||
appliedRules = append(appliedRules, gconv.String(mapping.SourceField)+" -> "+gconv.String(mapping.TargetField))
|
||||
}
|
||||
|
||||
return &dto.ExecuteDataMappingRes{
|
||||
TargetData: targetData,
|
||||
AppliedRules: appliedRules,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// applyTransformRule 应用转换规则
|
||||
func (s *dataMappingService) applyTransformRule(ctx context.Context, sourceValue interface{}, exists bool, mapping entity.DataMapping) interface{} {
|
||||
// 如果源字段不存在,使用默认值
|
||||
if !exists {
|
||||
if mapping.DefaultValue != "" {
|
||||
return mapping.DefaultValue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果没有转换规则,直接返回源值
|
||||
if mapping.TransformRule == nil || len(mapping.TransformRule) == 0 {
|
||||
return sourceValue
|
||||
}
|
||||
|
||||
// 获取转换类型
|
||||
transformType := ""
|
||||
if t, ok := mapping.TransformRule["type"].(string); ok {
|
||||
transformType = t
|
||||
}
|
||||
|
||||
// 根据转换类型应用不同的转换逻辑
|
||||
switch consts.TransformType(transformType) {
|
||||
case consts.TransformTypeFixed:
|
||||
// 固定值
|
||||
if v, ok := mapping.TransformRule["rule"].(string); ok {
|
||||
return v
|
||||
}
|
||||
case consts.TransformTypeMapping:
|
||||
// 值映射
|
||||
if mappingMap, ok := mapping.TransformRule["mappingMap"].(map[string]interface{}); ok {
|
||||
if sourceKey := gconv.String(sourceValue); sourceKey != "" {
|
||||
if v, ok := mappingMap[sourceKey]; ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
case consts.TransformTypeRegex:
|
||||
// 正则转换
|
||||
if regex, ok := mapping.TransformRule["regex"].(string); ok {
|
||||
// TODO: 实现正则替换逻辑
|
||||
g.Log().Warning(ctx, "正则转换暂未实现", g.Map{
|
||||
"regex": regex,
|
||||
"sourceValue": sourceValue,
|
||||
})
|
||||
}
|
||||
case consts.TransformTypeFunction:
|
||||
// 函数转换
|
||||
if functionName, ok := mapping.TransformRule["functionName"].(string); ok {
|
||||
// TODO: 实现函数调用逻辑
|
||||
g.Log().Warning(ctx, "函数转换暂未实现", g.Map{
|
||||
"function": functionName,
|
||||
"sourceValue": sourceValue,
|
||||
})
|
||||
}
|
||||
case consts.TransformTypeScript:
|
||||
// 脚本转换
|
||||
if script, ok := mapping.TransformRule["script"].(string); ok {
|
||||
// TODO: 实现脚本执行逻辑
|
||||
g.Log().Warning(ctx, "脚本转换暂未实现", g.Map{
|
||||
"script": script,
|
||||
"sourceValue": sourceValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 默认返回源值
|
||||
return sourceValue
|
||||
}
|
||||
|
||||
// getStatusName 获取状态名称
|
||||
func (s *dataMappingService) getStatusName(status consts.MappingStatus) string {
|
||||
statusNames := map[consts.MappingStatus]string{
|
||||
consts.MappingStatusActive: "启用",
|
||||
consts.MappingStatusInactive: "停用",
|
||||
}
|
||||
if name, ok := statusNames[status]; ok {
|
||||
return name
|
||||
}
|
||||
return string(status)
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"cid/consts"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type rateLimit struct{}
|
||||
|
||||
// RateLimit 限流服务
|
||||
var RateLimit = new(rateLimit)
|
||||
|
||||
// TenantRateLimitConfig 租户限流配置
|
||||
type TenantRateLimitConfig struct {
|
||||
TenantID int64 // 租户ID
|
||||
RequestsPerSecond float64 // 每秒请求数
|
||||
Burst int // 突发请求数
|
||||
Window time.Duration // 时间窗口
|
||||
}
|
||||
|
||||
// CheckTenantRequestLimit 检查租户请求次数限制
|
||||
func (s *rateLimit) CheckTenantRequestLimit(ctx context.Context, tenantID int64, config *TenantRateLimitConfig) (bool, error) {
|
||||
if config == nil {
|
||||
// 使用默认配置
|
||||
config = s.getDefaultTenantRateLimitConfig(tenantID)
|
||||
}
|
||||
|
||||
// 构建Redis键 - 使用当前小时的键,确保按小时计数
|
||||
now := time.Now()
|
||||
hourKey := fmt.Sprintf("%s%d:%d", consts.AdRequestLimitKeyPrefix, tenantID, now.Hour())
|
||||
|
||||
// 获取当前计数
|
||||
currentCountVar, err := g.Redis().Get(ctx, hourKey)
|
||||
if err != nil && err.Error() != "redis: nil" {
|
||||
return false, err
|
||||
}
|
||||
|
||||
currentCount := currentCountVar.Int64()
|
||||
|
||||
// 如果是第一次请求,设置计数和过期时间(到下一个小时)
|
||||
if currentCount == 0 {
|
||||
// 设置过期时间为到下一个小时的剩余时间
|
||||
nextHour := now.Truncate(time.Hour).Add(time.Hour)
|
||||
ttl := nextHour.Sub(now)
|
||||
// 使用SetEX一次性设置值和过期时间
|
||||
err = g.Redis().SetEX(ctx, hourKey, 1, int64(ttl.Seconds()))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 检查是否超过限制
|
||||
maxRequests := int64(config.RequestsPerSecond * config.Window.Seconds())
|
||||
if currentCount >= maxRequests {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 增加计数
|
||||
_, err = g.Redis().Incr(ctx, hourKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetDefaultTenantRateLimitConfig 获取默认的租户限流配置
|
||||
func (s *rateLimit) getDefaultTenantRateLimitConfig(tenantID int64) *TenantRateLimitConfig {
|
||||
// 从配置文件中读取限流参数
|
||||
ctx := context.Background()
|
||||
|
||||
// 检查是否启用租户限流
|
||||
enabled := g.Cfg().MustGet(ctx, "tenantRateLimit.enabled", false).Bool()
|
||||
if !enabled {
|
||||
// 如果未启用,返回一个很大的限制值,相当于不限制
|
||||
return &TenantRateLimitConfig{
|
||||
TenantID: tenantID,
|
||||
RequestsPerSecond: 10000, // 每秒10000个请求,相当于不限制
|
||||
Burst: 20000, // 突发20000个请求
|
||||
Window: time.Hour,
|
||||
}
|
||||
}
|
||||
|
||||
// 从配置文件中获取限流参数
|
||||
requestsPerHour := g.Cfg().MustGet(ctx, "tenantRateLimit.requestsPerHour", 3600).Int64()
|
||||
windowSeconds := g.Cfg().MustGet(ctx, "tenantRateLimit.window", 3600).Int64()
|
||||
burst := g.Cfg().MustGet(ctx, "tenantRateLimit.burst", 100).Int()
|
||||
|
||||
// 转换为每秒请求数
|
||||
requestsPerSecond := float64(requestsPerHour) / float64(windowSeconds)
|
||||
|
||||
return &TenantRateLimitConfig{
|
||||
TenantID: tenantID,
|
||||
RequestsPerSecond: requestsPerSecond,
|
||||
Burst: burst,
|
||||
Window: time.Duration(windowSeconds) * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// SetTenantRateLimitConfig 设置租户限流配置
|
||||
func (s *rateLimit) SetTenantRateLimitConfig(ctx context.Context, config *TenantRateLimitConfig) error {
|
||||
// 注意:实际使用的是config.yml中的全局配置,此方法仅用于兼容旧API
|
||||
// 实际限流参数请修改config.yml中的tenantRateLimit部分
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTenantCurrentUsage 获取租户当前请求使用情况
|
||||
func (s *rateLimit) GetTenantCurrentUsage(ctx context.Context, tenantID int64, config *TenantRateLimitConfig) (current int64, max int64, err error) {
|
||||
if config == nil {
|
||||
config = s.getDefaultTenantRateLimitConfig(tenantID)
|
||||
}
|
||||
|
||||
// 构建当前小时的Redis键
|
||||
now := time.Now()
|
||||
hourKey := fmt.Sprintf("%s%d:%d", consts.AdRequestLimitKeyPrefix, tenantID, now.Hour())
|
||||
|
||||
// 获取当前计数
|
||||
currentVar, err := g.Redis().Get(ctx, hourKey)
|
||||
if err != nil && err.Error() == "redis: nil" {
|
||||
current = 0
|
||||
err = nil
|
||||
} else if err != nil {
|
||||
return 0, 0, err
|
||||
} else {
|
||||
current = currentVar.Int64()
|
||||
}
|
||||
|
||||
max = int64(config.RequestsPerSecond * config.Window.Seconds())
|
||||
return current, max, nil
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"cid/dao"
|
||||
"cid/model/dto"
|
||||
"cid/model/entity"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type strategy struct{}
|
||||
|
||||
// Strategy 策略服务
|
||||
var Strategy = new(strategy)
|
||||
|
||||
// CreateStrategy 创建策略
|
||||
func (s *strategy) CreateStrategy(ctx context.Context, req *dto.CreateStrategyReq) (id int64, err error) {
|
||||
// 检查策略名称是否已存在
|
||||
existingStrategy, err := dao.Strategy.GetByName(ctx, req.Name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if existingStrategy != nil {
|
||||
return 0, gerror.New("策略名称已存在")
|
||||
}
|
||||
|
||||
// 验证转化率范围
|
||||
if req.MaxConversion <= req.MinConversion {
|
||||
return 0, gerror.New("最高转化率必须大于最低转化率")
|
||||
}
|
||||
|
||||
// 序列化权重配置
|
||||
weightsJson, err := json.Marshal(req.SourceWeights)
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "权重配置序列化失败")
|
||||
}
|
||||
|
||||
strategy := &entity.Strategy{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
MinConversion: req.MinConversion,
|
||||
MaxConversion: req.MaxConversion,
|
||||
SourceWeights: string(weightsJson),
|
||||
MaxAdsPerReq: req.MaxAdsPerReq,
|
||||
Priority: req.Priority,
|
||||
}
|
||||
|
||||
// 设置状态
|
||||
strategy.Status = req.Status
|
||||
|
||||
_, err = dao.Strategy.Create(ctx, strategy)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// MongoDB使用ObjectId,创建成功后返回成功状态
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// UpdateStrategy 更新策略
|
||||
func (s *strategy) UpdateStrategy(ctx context.Context, req *dto.UpdateStrategyReq) (affected int64, err error) {
|
||||
// 检查策略是否存在
|
||||
existingStrategy, err := dao.Strategy.GetByID(ctx, strconv.FormatInt(req.Id, 10))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if existingStrategy == nil {
|
||||
return 0, gerror.New("策略不存在")
|
||||
}
|
||||
|
||||
// 如果更新名称,检查是否与其他策略冲突
|
||||
if req.Name != "" && req.Name != existingStrategy.Name {
|
||||
conflictStrategy, err := dao.Strategy.GetByName(ctx, req.Name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if conflictStrategy != nil {
|
||||
return 0, gerror.New("策略名称已存在")
|
||||
}
|
||||
}
|
||||
|
||||
// 验证转化率范围
|
||||
if req.MaxConversion <= req.MinConversion {
|
||||
return 0, gerror.New("最高转化率必须大于最低转化率")
|
||||
}
|
||||
|
||||
// 序列化权重配置
|
||||
weightsJson, err := json.Marshal(req.SourceWeights)
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "权重配置序列化失败")
|
||||
}
|
||||
|
||||
strategy := &entity.Strategy{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
MinConversion: req.MinConversion,
|
||||
MaxConversion: req.MaxConversion,
|
||||
SourceWeights: string(weightsJson),
|
||||
MaxAdsPerReq: req.MaxAdsPerReq,
|
||||
Priority: req.Priority,
|
||||
}
|
||||
|
||||
// 设置状态
|
||||
strategy.Status = req.Status
|
||||
|
||||
return dao.Strategy.Update(ctx, strategy)
|
||||
}
|
||||
|
||||
// DeleteStrategy 删除策略
|
||||
func (s *strategy) DeleteStrategy(ctx context.Context, id int64) (affected int64, err error) {
|
||||
// 检查策略是否存在
|
||||
existingStrategy, err := dao.Strategy.GetByID(ctx, strconv.FormatInt(id, 10))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if existingStrategy == nil {
|
||||
return 0, gerror.New("策略不存在")
|
||||
}
|
||||
|
||||
return dao.Strategy.Delete(ctx, strconv.FormatInt(id, 10))
|
||||
}
|
||||
|
||||
// GetStrategyByID 根据ID获取策略
|
||||
func (s *strategy) GetStrategyByID(ctx context.Context, id int64) (strategy *dto.StrategyRes, err error) {
|
||||
entity, err := dao.Strategy.GetByID(ctx, strconv.FormatInt(id, 10))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entity == nil {
|
||||
return nil, gerror.New("策略不存在")
|
||||
}
|
||||
|
||||
// 反序列化权重配置
|
||||
var weights map[string]int
|
||||
if entity.SourceWeights != "" {
|
||||
err = json.Unmarshal([]byte(entity.SourceWeights), &weights)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "权重配置反序列化失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 将ObjectId的十六进制字符串转换为int64,如果失败则使用0
|
||||
var idInt64 int64
|
||||
if id, err := strconv.ParseInt(entity.Id.Hex(), 16, 64); err == nil {
|
||||
idInt64 = id
|
||||
}
|
||||
|
||||
return &dto.StrategyRes{
|
||||
Id: idInt64,
|
||||
Name: entity.Name,
|
||||
Description: entity.Description,
|
||||
TenantLevel: "", // Strategy实体中没有TenantLevel字段,暂时设为空字符串
|
||||
MinConversion: entity.MinConversion,
|
||||
MaxConversion: entity.MaxConversion,
|
||||
SourceWeights: weights,
|
||||
MaxAdsPerReq: entity.MaxAdsPerReq,
|
||||
Priority: entity.Priority,
|
||||
Status: entity.Status,
|
||||
CreatedAt: entity.CreatedAt.String(),
|
||||
UpdatedAt: entity.UpdatedAt.String(),
|
||||
CreatedBy: 0, // 这些字段在MongoBaseDO中不存在,暂时设为0
|
||||
UpdatedBy: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetStrategyList 获取策略列表
|
||||
func (s *strategy) GetStrategyList(ctx context.Context, req *dto.GetStrategyListReq) (res *dto.GetStrategyListRes, err error) {
|
||||
list, total, err := dao.Strategy.GetList(ctx, req.Page, req.Size, req.TenantLevel, req.Status)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var strategyList []*dto.StrategyRes
|
||||
for _, entity := range list {
|
||||
// 反序列化权重配置
|
||||
var weights map[string]int
|
||||
if entity.SourceWeights != "" {
|
||||
err = json.Unmarshal([]byte(entity.SourceWeights), &weights)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "策略 %d 权重配置反序列化失败: %v", entity.Id, err)
|
||||
weights = make(map[string]int)
|
||||
}
|
||||
}
|
||||
|
||||
// 将ObjectId的十六进制字符串转换为int64,如果失败则使用0
|
||||
var idInt64 int64
|
||||
if id, err := strconv.ParseInt(entity.Id.Hex(), 16, 64); err == nil {
|
||||
idInt64 = id
|
||||
}
|
||||
|
||||
strategyList = append(strategyList, &dto.StrategyRes{
|
||||
Id: idInt64,
|
||||
Name: entity.Name,
|
||||
Description: entity.Description,
|
||||
TenantLevel: "", // Strategy实体中没有TenantLevel字段,暂时设为空字符串
|
||||
MinConversion: entity.MinConversion,
|
||||
MaxConversion: entity.MaxConversion,
|
||||
SourceWeights: weights,
|
||||
MaxAdsPerReq: entity.MaxAdsPerReq,
|
||||
Priority: entity.Priority,
|
||||
Status: entity.Status,
|
||||
CreatedAt: entity.CreatedAt.String(),
|
||||
UpdatedAt: entity.UpdatedAt.String(),
|
||||
CreatedBy: 0, // 这些字段在MongoBaseDO中不存在,暂时设为0
|
||||
UpdatedBy: 0,
|
||||
})
|
||||
}
|
||||
|
||||
return &dto.GetStrategyListRes{
|
||||
List: strategyList,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
Size: req.Size,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetStrategyByTenantLevel 根据租户级别获取策略
|
||||
func (s *strategy) GetStrategyByTenantLevel(ctx context.Context, tenantLevel string) (strategy *entity.Strategy, err error) {
|
||||
return dao.Strategy.GetByTenantLevel(ctx, tenantLevel)
|
||||
}
|
||||
Reference in New Issue
Block a user