feat(camel_oil): 添加骆驼模块设置和预拉取订单日志功能

- 增加骆驼模块设置接口支持获取和更新配置
- 使用Redis缓存设置数据,实现模块配置的持久化管理
- 引入预拉取订单日志功能,支持日志的保存和按时间范围查询
- 预拉取订单请求响应数据记录到Redis,方便问题追踪
- 根据模块设置动态调整账号登录、预拉取订单并发数量
- 调整账号登录逻辑以支持配置的并发控制
- 优化预拉取订单补充流程,支持多面额库存管理
- 修正集成API请求函数名及调用,记录详细调用日志数据
- 调整定时任务调度频率,增加预拉取订单补充任务的执行频率
- 升级golang版本到1.25.5,保持开发环境最新状态
This commit is contained in:
danial
2025-12-03 21:17:56 +08:00
parent c0d25f8a15
commit 8495c453f3
18 changed files with 515 additions and 70 deletions

View File

@@ -1,2 +1,2 @@
golang 1.25.3 golang 1.25.5
python 3.13.9 python 3.13.9

View File

@@ -21,6 +21,8 @@ type ICamelOilV1 interface {
OrderHistory(ctx context.Context, req *v1.OrderHistoryReq) (res *v1.OrderHistoryRes, err error) OrderHistory(ctx context.Context, req *v1.OrderHistoryReq) (res *v1.OrderHistoryRes, err error)
AccountOrderList(ctx context.Context, req *v1.AccountOrderListReq) (res *v1.AccountOrderListRes, err error) AccountOrderList(ctx context.Context, req *v1.AccountOrderListReq) (res *v1.AccountOrderListRes, err error)
OrderCallback(ctx context.Context, req *v1.OrderCallbackReq) (res *v1.OrderCallbackRes, err error) OrderCallback(ctx context.Context, req *v1.OrderCallbackReq) (res *v1.OrderCallbackRes, err error)
GetSettings(ctx context.Context, req *v1.GetSettingsReq) (res *v1.GetSettingsRes, err error)
UpdateSettings(ctx context.Context, req *v1.UpdateSettingsReq) (res *v1.UpdateSettingsRes, err error)
CreateToken(ctx context.Context, req *v1.CreateTokenReq) (res *v1.CreateTokenRes, err error) CreateToken(ctx context.Context, req *v1.CreateTokenReq) (res *v1.CreateTokenRes, err error)
GetToken(ctx context.Context, req *v1.GetTokenReq) (res *v1.GetTokenRes, err error) GetToken(ctx context.Context, req *v1.GetTokenReq) (res *v1.GetTokenRes, err error)
ListTokens(ctx context.Context, req *v1.ListTokensReq) (res *v1.ListTokensRes, err error) ListTokens(ctx context.Context, req *v1.ListTokensReq) (res *v1.ListTokensRes, err error)

View File

@@ -0,0 +1,33 @@
package v1
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// GetPrefetchOrderLogsReq 获取预拉取订单日志请求
type GetPrefetchOrderLogsReq struct {
g.Meta `path:"/jd-v2/prefetch/logs" tags:"JD V2 Prefetch" method:"get" summary:"获取预拉取订单日志"`
// 开始时间
StartTime *gtime.Time `json:"startTime" v:"required#开始时间不能为空" description:"开始时间"`
// 结束时间
EndTime *gtime.Time `json:"endTime" v:"required#结束时间不能为空" description:"结束时间"`
}
// PrefetchOrderLogItem 预拉取订单日志项
type PrefetchOrderLogItem struct {
// 请求时间戳
Timestamp string `json:"timestamp" description:"请求时间戳"`
// 手机号(脱敏)
Phone string `json:"phone" description:"手机号(脱敏)"`
// 订单面额
Amount float64 `json:"amount" description:"订单面额"`
// API响应数据
ResponseData string `json:"responseData" description:"API响应原始数据"`
}
// GetPrefetchOrderLogsRes 获取预拉取订单日志响应
type GetPrefetchOrderLogsRes struct {
// 日志列表
Logs []PrefetchOrderLogItem `json:"logs" description:"预拉取订单日志列表"`
}

View File

@@ -0,0 +1,50 @@
package v1
import (
"github.com/gogf/gf/v2/frame/g"
)
// GetSettingsReq 获取骆驼模块设置
type GetSettingsReq struct {
g.Meta `path:"/jd-v2/settings/get" tags:"JD V2 Settings" method:"get" summary:"获取骆驼模块设置"`
}
type GetSettingsRes struct {
g.Meta `mime:"application/json"`
CamelOilSettings
}
// UpdateSettingsReq 更新骆驼模块设置
type UpdateSettingsReq struct {
g.Meta `path:"/jd-v2/settings/update" tags:"JD V2 Settings" method:"post" summary:"更新骆驼模块设置"`
CamelOilSettings
}
type UpdateSettingsRes struct {
g.Meta `mime:"application/json"`
}
// DenominationSetting 单个面额设置
type DenominationSetting struct {
Denomination int `json:"denomination" description:"面额值如100、200、500等"`
MinCapacity int `json:"minCapacity" description:"该面额预拉取订单最小库存阈值(当库存低于此值时触发补充)"`
TargetCapacity int `json:"targetCapacity" description:"该面额预拉取订单目标库存(补充时的目标数量)"`
}
// CamelOilSettings 骆驼模块设置
type CamelOilSettings struct {
// 豪猪平台相关设置
UseHaozhuPlatform bool `json:"useHaozhuPlatform" description:"是否从豪猪平台获取手机号登录"`
// 账号登录数量设置
LoginAccountCount int `json:"loginAccountCount" description:"要登录的手机号数量"`
// 提前拉单并发设置
PrefetchConcurrencyAccounts int `json:"prefetchConcurrencyAccounts" description:"提前拉单并发的账号数量"`
// 单账号并发设置
SingleAccountConcurrency int `json:"singleAccountConcurrency" description:"单个账号的并发数量"`
// 面额相关设置
TargetDenominations []DenominationSetting `json:"targetDenominations" description:"要获取的面额和对应库存设置列表"`
}

View File

@@ -210,18 +210,9 @@ var CamelOilPrefetchOrderChangeTypeText = map[CamelOilPrefetchOrderChangeType]st
// ==================================================================================== // ====================================================================================
const ( const (
// CamelOilPrefetchOrderMinCapacity 预拉取订单最小库存阈值(当库存低于此值时触发补充)
CamelOilPrefetchOrderMinCapacity = 1
// CamelOilPrefetchOrderTargetCapacity 预拉取订单目标库存(补充时的目标数量)
CamelOilPrefetchOrderTargetCapacity = 5
// CamelOilPrefetchOrderExpireDuration 预拉取订单过期时间(小时) // CamelOilPrefetchOrderExpireDuration 预拉取订单过期时间(小时)
CamelOilPrefetchOrderExpireDuration = time.Hour * 24 CamelOilPrefetchOrderExpireDuration = time.Hour * 24
// CamelOilPrefetchMaxConcurrency 预拉取最大并发账号数量
CamelOilPrefetchMaxConcurrency = 10
// CamelOilPrefetchOrderLockKey Redis中预拉取订单的分布式锁键名前缀 // CamelOilPrefetchOrderLockKey Redis中预拉取订单的分布式锁键名前缀
CamelOilPrefetchOrderLockKey = "camel_oil_api:prefetch:order:lock:" CamelOilPrefetchOrderLockKey = "camel_oil_api:prefetch:order:lock:"
@@ -237,18 +228,12 @@ const (
// CamelOilAccountDailyOrderLimit 账号每日订单上限 // CamelOilAccountDailyOrderLimit 账号每日订单上限
CamelOilAccountDailyOrderLimit = 10 CamelOilAccountDailyOrderLimit = 10
// CamelOilTargetOnlineAccounts 目标在线账号数量
CamelOilTargetOnlineAccounts = 10
// CamelOilOrderExpireDuration 订单支付超时时间(小时) // CamelOilOrderExpireDuration 订单支付超时时间(小时)
CamelOilOrderExpireDuration = gtime.H CamelOilOrderExpireDuration = gtime.H
// CamelOilMaxCallbackRetry 回调最大重试次数 // CamelOilMaxCallbackRetry 回调最大重试次数
CamelOilMaxCallbackRetry = 3 CamelOilMaxCallbackRetry = 3
// CamelOilMaxLoginConcurrency 最大并发登录数量
CamelOilMaxLoginConcurrency = 3
// CamelOilTokenExpireDuration Token过期时间 // CamelOilTokenExpireDuration Token过期时间
CamelOilTokenExpireDuration = 30 CamelOilTokenExpireDuration = 30
) )

View File

@@ -0,0 +1,12 @@
package camel_oil
import (
"context"
v1 "kami/api/camel_oil/v1"
"kami/internal/service"
)
func (c *ControllerV1) GetPrefetchOrderLogs(ctx context.Context, req *v1.GetPrefetchOrderLogsReq) (res *v1.GetPrefetchOrderLogsRes, err error) {
return service.CamelOil().GetPrefetchOrderLogs(ctx, req)
}

View File

@@ -0,0 +1,12 @@
package camel_oil
import (
"context"
"kami/api/camel_oil/v1"
"kami/internal/service"
)
func (c *ControllerV1) GetSettings(ctx context.Context, req *v1.GetSettingsReq) (res *v1.GetSettingsRes, err error) {
return service.CamelOil().GetSettings(ctx, req)
}

View File

@@ -0,0 +1,12 @@
package camel_oil
import (
"context"
"kami/api/camel_oil/v1"
"kami/internal/service"
)
func (c *ControllerV1) UpdateSettings(ctx context.Context, req *v1.UpdateSettingsReq) (res *v1.UpdateSettingsRes, err error) {
return service.CamelOil().UpdateSettings(ctx, req)
}

View File

@@ -9,6 +9,7 @@ import (
"kami/utility/config" "kami/utility/config"
"kami/utility/integration/camel_oil_api" "kami/utility/integration/camel_oil_api"
"kami/utility/integration/pig" "kami/utility/integration/pig"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -19,13 +20,25 @@ import (
// LoginAccount 执行账号登录流程 // LoginAccount 执行账号登录流程
// 注意:当前使用假数据,实际应对接骆驼加油平台和接码平台 // 注意:当前使用假数据,实际应对接骆驼加油平台和接码平台
func (s *sCamelOil) LoginAccount(ctx context.Context) (err error) { func (s *sCamelOil) LoginAccount(ctx context.Context) (err error) {
// 获取设置
settings, err := GetCamelOilSettings(ctx)
if err != nil {
glog.Errorf(ctx, "获取骆驼模块设置失败: %v", err)
return err
}
// 如果不使用豪猪平台,直接返回错误
if !settings.UseHaozhuPlatform {
return gerror.New("未启用豪猪平台,无法获取手机号")
}
// 对接接码平台,获取手机号并检查是否已存在 // 对接接码平台,获取手机号并检查是否已存在
var phoneNumber string var phoneNumber string
ticker := time.NewTicker(time.Second) ticker := time.NewTicker(time.Second)
for range ticker.C { for range ticker.C {
phoneNumber, err = pig.NewClient().GetAccountInfo(ctx) phoneNumber, err = pig.NewClient().GetAccountInfo(ctx)
if err != nil { if err != nil {
return gerror.Wrap(err, "获取手机号失败") return gerror.Wrap(err, "从豪猪平台获取手机号失败")
} }
// 检查手机号是否已存在 // 检查手机号是否已存在
@@ -67,21 +80,36 @@ func (s *sCamelOil) BatchLoginAccounts(ctx context.Context, count int64) (succes
return 0, gerror.New("登录数量必须大于0") return 0, gerror.New("登录数量必须大于0")
} }
// 逐个登录账号 // 获取设置
successCount = 0 settings, err := GetCamelOilSettings(ctx)
for range 10 { if err != nil {
if successCount >= count { glog.Errorf(ctx, "获取骆驼模块设置失败: %v", err)
break return 0, err
} }
for i := 0; i < int(count-successCount); i++ {
// 使用设置中的并发数量控制登录
semaphore := make(chan struct{}, settings.SingleAccountConcurrency)
var wg sync.WaitGroup
var successCounter int64
for i := 0; i < int(count); i++ {
wg.Add(1)
go func() {
defer wg.Done()
semaphore <- struct{}{} // 获取信号量
defer func() { <-semaphore }() // 释放信号量
loginErr := s.LoginAccount(ctx) loginErr := s.LoginAccount(ctx)
if loginErr != nil { if loginErr != nil {
glog.Errorf(ctx, "账号登录失败,错误: %v", loginErr) glog.Errorf(ctx, "账号登录失败,错误: %v", loginErr)
return return
} }
atomic.AddInt64(&successCount, 1) atomic.AddInt64(&successCounter, 1)
} }()
} }
wg.Wait()
successCount = successCounter
glog.Infof(ctx, "批量登录完成,成功: %d", successCount) glog.Infof(ctx, "批量登录完成,成功: %d", successCount)
return successCount, nil return successCount, nil
} }

View File

@@ -26,6 +26,13 @@ func (s *sCamelOil) CronAccountPrefetchTask(ctx context.Context) error {
return nil return nil
} }
// 获取设置
settings, err := GetCamelOilSettings(ctx)
if err != nil {
glog.Errorf(ctx, "获取骆驼模块设置失败: %v", err)
return err
}
// 1. 获取当前在线账号数量 // 1. 获取当前在线账号数量
m := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1()) m := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1())
onlineCount, err := m.Where(dao.V1CamelOilAccount.Columns().Status, consts.CamelOilAccountStatusOnline). onlineCount, err := m.Where(dao.V1CamelOilAccount.Columns().Status, consts.CamelOilAccountStatusOnline).
@@ -36,11 +43,12 @@ func (s *sCamelOil) CronAccountPrefetchTask(ctx context.Context) error {
onlineCount = 0 onlineCount = 0
} }
glog.Infof(ctx, "当前在线账号数量: %d, 目标数量: %d", onlineCount, consts.CamelOilTargetOnlineAccounts) targetOnlineAccounts := settings.LoginAccountCount
glog.Infof(ctx, "当前在线账号数量: %d, 目标数量: %d", onlineCount, targetOnlineAccounts)
// 2. 如果在线账号少于目标数,触发并发登录 // 2. 如果在线账号少于目标数,触发并发登录
if onlineCount < consts.CamelOilTargetOnlineAccounts { if onlineCount < targetOnlineAccounts {
needCount := consts.CamelOilTargetOnlineAccounts - onlineCount needCount := targetOnlineAccounts - onlineCount
glog.Infof(ctx, "在线账号不足,需要登录 %d 个账号", needCount) glog.Infof(ctx, "在线账号不足,需要登录 %d 个账号", needCount)
// 使用并发登录提高效率 // 使用并发登录提高效率

View File

@@ -44,6 +44,13 @@ func (s *sCamelOil) GetPrefetchOrderCapacity(ctx context.Context, amount float64
// PrefetchOrderConcurrently 使用所有可用账号并发拉取订单,直到获取到可用订单为止 // PrefetchOrderConcurrently 使用所有可用账号并发拉取订单,直到获取到可用订单为止
func (s *sCamelOil) PrefetchOrderConcurrently(ctx context.Context, amount float64) (result *model.PrefetchOrderResult, err error) { func (s *sCamelOil) PrefetchOrderConcurrently(ctx context.Context, amount float64) (result *model.PrefetchOrderResult, err error) {
// 获取设置
settings, err := GetCamelOilSettings(ctx)
if err != nil {
glog.Errorf(ctx, "获取骆驼模块设置失败: %v", err)
return nil, err
}
// 1. 获取所有在线账号 // 1. 获取所有在线账号
m := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1()) m := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1())
var onlineAccounts []*entity.V1CamelOilAccount var onlineAccounts []*entity.V1CamelOilAccount
@@ -59,8 +66,8 @@ func (s *sCamelOil) PrefetchOrderConcurrently(ctx context.Context, amount float6
return nil, gerror.New("暂无在线账号可用") return nil, gerror.New("暂无在线账号可用")
} }
// 2. 使用控制并发量的信信道控制并发 // 2. 使用设置中的并发数量限制并发
concurrencyLimit := min(len(onlineAccounts), consts.CamelOilPrefetchMaxConcurrency) concurrencyLimit := min(len(onlineAccounts), settings.PrefetchConcurrencyAccounts)
var ( var (
resultChan = make(chan *model.PrefetchOrderResult, 1) resultChan = make(chan *model.PrefetchOrderResult, 1)
@@ -88,7 +95,7 @@ func (s *sCamelOil) PrefetchOrderConcurrently(ctx context.Context, amount float6
mu.Unlock() mu.Unlock()
// 拉取订单 // 拉取订单
platformOrderId, payUrl, err2 := camel_oil_api.NewClient().CreateOrder(ctx, acc.Phone, acc.Token, amount) platformOrderId, payUrl, err2 := camel_oil_api.NewClient().CreateCamelOilOrder(ctx, acc.Phone, acc.Token, amount)
if err2 != nil { if err2 != nil {
if err2.Error() == "auth_error" { if err2.Error() == "auth_error" {
_ = s.UpdateAccountStatus(ctx, acc.Id, consts.CamelOilAccountStatusInvalid, consts.CamelOilAccountChangeTypeInvalidate, "账号token失效") _ = s.UpdateAccountStatus(ctx, acc.Id, consts.CamelOilAccountStatusInvalid, consts.CamelOilAccountChangeTypeInvalidate, "账号token失效")
@@ -138,7 +145,7 @@ func (s *sCamelOil) PrefetchOrderConcurrently(ctx context.Context, amount float6
// PrefetchOrder 拉取单个订单(用于单个账号) // PrefetchOrder 拉取单个订单(用于单个账号)
func (s *sCamelOil) PrefetchOrder(ctx context.Context, account *entity.V1CamelOilAccount, amount float64) (prefetchId int64, err error) { func (s *sCamelOil) PrefetchOrder(ctx context.Context, account *entity.V1CamelOilAccount, amount float64) (prefetchId int64, err error) {
// 1. 从骆驼平台拉取订单 // 1. 从骆驼平台拉取订单
platformOrderId, payUrl, err := camel_oil_api.NewClient().CreateOrder(ctx, account.Phone, account.Token, amount) platformOrderId, payUrl, err := camel_oil_api.NewClient().CreateCamelOilOrder(ctx, account.Phone, account.Token, amount)
if err != nil { if err != nil {
if err.Error() == "auth_error" { if err.Error() == "auth_error" {
_ = s.UpdateAccountStatus(ctx, account.Id, consts.CamelOilAccountStatusInvalid, consts.CamelOilAccountChangeTypeInvalidate, "账号token失效") _ = s.UpdateAccountStatus(ctx, account.Id, consts.CamelOilAccountStatusInvalid, consts.CamelOilAccountChangeTypeInvalidate, "账号token失效")
@@ -178,6 +185,13 @@ func (s *sCamelOil) PrefetchOrder(ctx context.Context, account *entity.V1CamelOi
// ConcurrentPrefetchOrders 使用多个账号并发拉取订单 // ConcurrentPrefetchOrders 使用多个账号并发拉取订单
func (s *sCamelOil) ConcurrentPrefetchOrders(ctx context.Context, amount float64, targetCount int) (successCount int, err error) { func (s *sCamelOil) ConcurrentPrefetchOrders(ctx context.Context, amount float64, targetCount int) (successCount int, err error) {
// 获取设置
settings, err := GetCamelOilSettings(ctx)
if err != nil {
glog.Errorf(ctx, "获取骆驼模块设置失败: %v", err)
return 0, err
}
// 1. 获取所有在线账号 // 1. 获取所有在线账号
m := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1()) m := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1())
var onlineAccounts []*entity.V1CamelOilAccount var onlineAccounts []*entity.V1CamelOilAccount
@@ -193,11 +207,8 @@ func (s *sCamelOil) ConcurrentPrefetchOrders(ctx context.Context, amount float64
return 0, gerror.New("暂无在线账号可用于拉取订单") return 0, gerror.New("暂无在线账号可用于拉取订单")
} }
// 2. 使用协程池并发拉取 // 2. 使用设置中的并发数量限制并发
concurrencyLimit := consts.CamelOilPrefetchMaxConcurrency concurrencyLimit := settings.PrefetchConcurrencyAccounts
if len(onlineAccounts) < concurrencyLimit {
concurrencyLimit = len(onlineAccounts)
}
var ( var (
wg sync.WaitGroup wg sync.WaitGroup
@@ -274,42 +285,46 @@ func (s *sCamelOil) SupplementPrefetchOrders(ctx context.Context) (supplementedC
gmlock.Lock(consts.CamelOilPrefetchTaskLockKey) gmlock.Lock(consts.CamelOilPrefetchTaskLockKey)
defer gmlock.Unlock(consts.CamelOilPrefetchTaskLockKey) defer gmlock.Unlock(consts.CamelOilPrefetchTaskLockKey)
//找到一个可用账户 // 获取设置
account := &entity.V1CamelOilAccount{} settings, err := GetCamelOilSettings(ctx)
_ = dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilAccount.Columns().Status, consts.CamelOilAccountStatusOnline).
OrderRandom().
Scan(&account)
goods, err := camel_oil_api.NewClient().QueryAvailableDenominations(ctx, account.Token)
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "查询可用面额失败") glog.Errorf(ctx, "获取骆驼模块设置失败: %v", err)
return 0, err
} }
// 如果没有设置面额配置,直接返回
if len(settings.TargetDenominations) == 0 {
glog.Infof(ctx, "未配置面额设置,无需补充预拉取订单")
return 0, nil
}
successCount := 0 successCount := 0
for _, good := range goods { for _, denom := range settings.TargetDenominations {
// 1. 获取当前库存 // 1. 获取当前库存
capacity, err2 := s.GetPrefetchOrderCapacity(ctx, good.GoodPrice) capacity, err2 := s.GetPrefetchOrderCapacity(ctx, float64(denom.Denomination))
if err2 != nil { if err2 != nil {
return 0, gerror.Wrap(err2, "获取预拉取订单库存失败") return 0, gerror.Wrap(err2, "获取预拉取订单库存失败")
} }
glog.Infof(ctx, "当前预拉取订单库存: %d", capacity) glog.Infof(ctx, "当前预拉取订单库存 (面额 %d): %d", denom.Denomination, capacity)
// 2. 如果库存充足,无需补充 // 2. 如果库存充足,无需补充
if capacity >= consts.CamelOilPrefetchOrderMinCapacity { if capacity >= denom.MinCapacity {
glog.Infof(ctx, "预拉取订单库存充足 (%d >= %d),无需补充", capacity, consts.CamelOilPrefetchOrderMinCapacity) glog.Infof(ctx, "面额 %d 预拉取订单库存充足 (%d >= %d),无需补充", denom.Denomination, capacity, denom.MinCapacity)
return 0, nil continue
} }
// 3. 计算需要补充的数量 // 3. 计算需要补充的数量
needCount := consts.CamelOilPrefetchOrderTargetCapacity - capacity needCount := denom.TargetCapacity - capacity
glog.Infof(ctx, "预拉取订单库存不足,需要补充 %d 单,金额: 100元", needCount) glog.Infof(ctx, "面额 %d 预拉取订单库存不足,需要补充 %d 单", denom.Denomination, needCount)
// 4. 并发拉取订单 // 4. 并发拉取订单
successCount, err = s.ConcurrentPrefetchOrders(ctx, good.GoodPrice, needCount) success, err := s.ConcurrentPrefetchOrders(ctx, float64(denom.Denomination), needCount)
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "并发拉取订单失败") glog.Errorf(ctx, "面额 %d 并发拉取订单失败: %v", denom.Denomination, err)
continue
} }
successCount += success
} }
return successCount, nil return successCount, nil
} }

View File

@@ -0,0 +1,165 @@
package camel_oil
import (
"context"
"encoding/json"
"fmt"
"time"
v1 "kami/api/camel_oil/v1"
"kami/utility/cache"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/os/gtime"
)
// GetPrefetchOrderLogs 获取预拉取订单日志
func (s *sCamelOil) GetPrefetchOrderLogs(ctx context.Context, req *v1.GetPrefetchOrderLogsReq) (res *v1.GetPrefetchOrderLogsRes, err error) {
// 计算时间范围跨度
duration := req.EndTime.Time.Sub(req.StartTime.Time)
if duration <= 0 {
return nil, gerror.New("结束时间必须大于开始时间")
}
// 限制查询时间范围不超过24小时
if duration.Hours() > 24 {
return nil, gerror.New("查询时间范围不能超过24小时")
}
glog.Infof(ctx, "获取预拉取订单日志,时间范围: %s - %s", req.StartTime.Format("Y-m-d H:i:s"), req.EndTime.Format("Y-m-d H:i:s"))
// Redis key 前缀
redisKeyPrefix := "camel_oil:prefetch:logs:"
var logs []v1.PrefetchOrderLogItem
// 遍历时间范围内的每一分钟
currentTime := req.StartTime.Time
for currentTime.Before(req.EndTime.Time) || currentTime.Equal(req.EndTime.Time) {
timeKey := currentTime.Format("2006-01-02_15:04")
redisKey := redisKeyPrefix + timeKey
// 从Redis获取日志数据
logData, err := cache.NewCache().Get(ctx, redisKey)
if err != nil {
glog.Warningf(ctx, "获取Redis日志失败key: %s, error: %v", redisKey, err)
currentTime = currentTime.Add(time.Minute)
continue
}
if logData.IsEmpty() {
currentTime = currentTime.Add(time.Minute)
continue
}
// 解析日志数据
var minuteLogs []map[string]interface{}
if err := json.Unmarshal([]byte(logData.String()), &minuteLogs); err != nil {
glog.Warningf(ctx, "解析日志数据失败key: %s, error: %v", redisKey, err)
currentTime = currentTime.Add(time.Minute)
continue
}
// 处理每条日志
for _, log := range minuteLogs {
// 提取面额
amount, ok := log["amount"].(float64)
if !ok {
continue
}
// 提取手机号
phone, ok := log["phone"].(string)
if !ok {
continue
}
// 提取时间戳
timestamp, ok := log["timestamp"].(string)
if !ok {
continue
}
// 提取响应数据
respStr, ok := log["resp_str"].(string)
if !ok {
respStr = ""
}
// 添加到结果列表,直接使用结构化字段
logs = append(logs, v1.PrefetchOrderLogItem{
Timestamp: timestamp,
Phone: phone,
Amount: amount,
ResponseData: respStr,
})
}
currentTime = currentTime.Add(time.Minute)
}
glog.Infof(ctx, "获取到预拉取订单日志 %d 条", len(logs))
// 返回结果
res = &v1.GetPrefetchOrderLogsRes{
Logs: logs,
}
return res, nil
}
// SavePrefetchOrderLog 保存预拉取订单请求日志到Redis
func (s *sCamelOil) SavePrefetchOrderLog(ctx context.Context, phone string, amount float64, respStr string) {
// 构建日志数据
logEntry := map[string]interface{}{
"timestamp": gtime.Now().Format("Y-m-d H:i:s"),
"phone": phone, // 实际使用时应该脱敏处理
"amount": amount,
"resp_str": respStr, // 保存响应数据
}
// 将日志数据序列化为JSON
logData, jsonErr := json.Marshal(logEntry)
if jsonErr != nil {
glog.Errorf(ctx, "序列化预拉取订单日志失败: %v", jsonErr)
return
}
// 生成Redis key (按分钟级别)
now := gtime.Now()
timeKey := now.Format("2006-01-02_15:04")
redisKey := fmt.Sprintf("camel_oil:prefetch:logs:%s", timeKey)
// 获取当前分钟已有的日志
var logs []map[string]interface{}
existingData, cacheErr := cache.NewCache().Get(ctx, redisKey)
if cacheErr == nil && !existingData.IsEmpty() {
if err := json.Unmarshal([]byte(existingData.String()), &logs); err != nil {
// 如果解析失败,创建新的日志数组
logs = []map[string]interface{}{}
}
}
// 添加新的日志
var newLog map[string]interface{}
if err := json.Unmarshal(logData, &newLog); err == nil {
logs = append(logs, newLog)
}
// 重新序列化并保存到Redis设置1小时过期时间
updatedLogData, marshalErr := json.Marshal(logs)
if marshalErr != nil {
glog.Errorf(ctx, "重新序列化日志失败: %v", marshalErr)
return
}
// 使用cache包保存到Redis
if cacheErr := cache.NewCache().Set(ctx, redisKey, string(updatedLogData), time.Hour); cacheErr != nil {
glog.Errorf(ctx, "保存预拉取订单日志到Redis失败: %v", cacheErr)
return
}
// 记录到应用日志
glog.Infof(ctx, "保存预拉取订单日志 - 手机号: %s, 金额: %.2f", phone, amount)
}

View File

@@ -0,0 +1,97 @@
package camel_oil
import (
"context"
"encoding/json"
"github.com/gogf/gf/v2/frame/g"
"kami/api/camel_oil/v1"
"kami/utility/cache"
)
// GetSettings 获取骆驼模块设置
func (s *sCamelOil) GetSettings(ctx context.Context, req *v1.GetSettingsReq) (res *v1.GetSettingsRes, err error) {
// 从Redis获取设置
settingsKey := cache.CamelOilSettings.Key("default")
c := cache.NewCache()
settingsData, err := c.Get(ctx, settingsKey)
if err != nil {
return nil, err
}
settings := &v1.CamelOilSettings{
UseHaozhuPlatform: false, // 默认不使用豪猪平台拉取手机号
LoginAccountCount: 0,
PrefetchConcurrencyAccounts: 0,
SingleAccountConcurrency: 1,
TargetDenominations: []v1.DenominationSetting{},
}
if settingsData != nil && !settingsData.IsNil() {
// 如果有缓存数据,解析它
err = json.Unmarshal([]byte(settingsData.String()), settings)
if err != nil {
g.Log().Error(ctx, "解析骆驼模块设置失败", err)
// 解析失败则使用默认值
}
}
res = &v1.GetSettingsRes{
CamelOilSettings: *settings,
}
return res, nil
}
// UpdateSettings 更新骆驼模块设置
func (s *sCamelOil) UpdateSettings(ctx context.Context, req *v1.UpdateSettingsReq) (res *v1.UpdateSettingsRes, err error) {
// 将设置序列化为JSON
settingsJSON, err := json.Marshal(req.CamelOilSettings)
if err != nil {
return nil, err
}
// 保存到Redis永不过期设置0表示永不过期
settingsKey := cache.CamelOilSettings.Key("default")
c := cache.NewCache()
err = c.Set(ctx, settingsKey, string(settingsJSON), 0)
if err != nil {
return nil, err
}
g.Log().Info(ctx, "骆驼模块设置已更新", string(settingsJSON))
res = &v1.UpdateSettingsRes{}
return res, nil
}
// GetCamelOilSettings 获取骆驼模块设置的辅助函数
func GetCamelOilSettings(ctx context.Context) (*v1.CamelOilSettings, error) {
settingsKey := cache.CamelOilSettings.Key("default")
c := cache.NewCache()
settingsData, err := c.Get(ctx, settingsKey)
if err != nil {
return nil, err
}
// 默认设置
settings := &v1.CamelOilSettings{
UseHaozhuPlatform: false, // 默认不使用豪猪平台拉取手机号
LoginAccountCount: 10, // 默认10个账号
PrefetchConcurrencyAccounts: 10, // 默认10个并发
SingleAccountConcurrency: 3, // 默认3个并发
TargetDenominations: []v1.DenominationSetting{},
}
if settingsData != nil && !settingsData.IsNil() {
err = json.Unmarshal([]byte(settingsData.String()), settings)
if err != nil {
g.Log().Error(ctx, "解析骆驼模块设置失败", err)
// 解析失败则使用默认值
}
}
return settings, nil
}

View File

@@ -105,6 +105,14 @@ type (
SupplementPrefetchOrders(ctx context.Context) (supplementedCount int, err error) SupplementPrefetchOrders(ctx context.Context) (supplementedCount int, err error)
// MatchPrefetchOrder 将预拉取订单与用户订单进行匹配 // MatchPrefetchOrder 将预拉取订单与用户订单进行匹配
MatchPrefetchOrder(ctx context.Context, orderId string, amount float64) (result *model.PrefetchOrderResult, err error) MatchPrefetchOrder(ctx context.Context, orderId string, amount float64) (result *model.PrefetchOrderResult, err error)
// GetPrefetchOrderLogs 获取预拉取订单日志
GetPrefetchOrderLogs(ctx context.Context, req *v1.GetPrefetchOrderLogsReq) (res *v1.GetPrefetchOrderLogsRes, err error)
// SavePrefetchOrderLog 保存预拉取订单请求日志到Redis
SavePrefetchOrderLog(ctx context.Context, phone string, amount float64, respStr string)
// GetSettings 获取骆驼模块设置
GetSettings(ctx context.Context, req *v1.GetSettingsReq) (res *v1.GetSettingsRes, err error)
// UpdateSettings 更新骆驼模块设置
UpdateSettings(ctx context.Context, req *v1.UpdateSettingsReq) (res *v1.UpdateSettingsRes, err error)
// CreateToken 创建 Token // CreateToken 创建 Token
CreateToken(ctx context.Context, tokenName string, tokenValue string, phone string, remark string, rechargeLimitAmount float64, rechargeLimitCount int) (tokenId int64, err error) CreateToken(ctx context.Context, tokenName string, tokenValue string, phone string, remark string, rechargeLimitAmount float64, rechargeLimitCount int) (tokenId int64, err error)
// GetTokenInfo 获取 Token 信息 // GetTokenInfo 获取 Token 信息

View File

@@ -18,6 +18,7 @@ const (
RedeemAccountTargetIDByUser CachedEnum = "redeem_account_target_id_by_user" RedeemAccountTargetIDByUser CachedEnum = "redeem_account_target_id_by_user"
RedeemAccountTargetIDByCKAndUser CachedEnum = "redeem_account_target_account_id_by_ck_and_user" RedeemAccountTargetIDByCKAndUser CachedEnum = "redeem_account_target_account_id_by_ck_and_user"
RedeemAccountTmpStopped CachedEnum = "redeem_account_tmp_stopped" RedeemAccountTmpStopped CachedEnum = "redeem_account_tmp_stopped"
CamelOilSettings CachedEnum = "camel_oil_settings"
) )
func (e CachedEnum) Key(key interface{}) string { func (e CachedEnum) Key(key interface{}) string {

View File

@@ -97,9 +97,9 @@ func registerCamelOilTasks(ctx context.Context) {
_ = service.CamelOil().CronAccountDailyResetTask(ctx) _ = service.CamelOil().CronAccountDailyResetTask(ctx)
}, "CamelOilAccountDailyReset") }, "CamelOilAccountDailyReset")
_, _ = gcron.AddSingleton(ctx, "@every 60m", func(ctx context.Context) { //_, _ = gcron.AddSingleton(ctx, "@every 60m", func(ctx context.Context) {
_, _ = service.CamelOil().CronCleanExpiredPrefetchOrders(ctx) // _, _ = service.CamelOil().CronCleanExpiredPrefetchOrders(ctx)
}, "CamelOilCleanExpiredPrefetchOrders") //}, "CamelOilCleanExpiredPrefetchOrders")
_, _ = gcron.AddSingleton(ctx, "@every 1s", func(ctx context.Context) { _, _ = gcron.AddSingleton(ctx, "@every 1s", func(ctx context.Context) {
_ = service.CamelOil().CronPrefetchOrderSupplementTask(ctx) _ = service.CamelOil().CronPrefetchOrderSupplementTask(ctx)

View File

@@ -6,6 +6,8 @@ import (
"errors" "errors"
"github.com/gogf/gf/v2/net/gclient" "github.com/gogf/gf/v2/net/gclient"
"github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/glog"
"kami/api/camel_oil/v1"
"kami/internal/service"
"math" "math"
"math/rand" "math/rand"
"strings" "strings"
@@ -165,8 +167,8 @@ func (c *Client) getAuth(ctx context.Context, auth string) string {
return authRes return authRes
} }
// QueryAvailableDenominations 查询所有可用面额 // QueryCamelOilCardAvailableDenominations 查询所有可用面额
func (c *Client) QueryAvailableDenominations(ctx context.Context, token string) ([]Good, error) { func (c *Client) QueryCamelOilCardAvailableDenominations(ctx context.Context, token string) ([]Good, error) {
c.Client.SetHeader("authorization", "Bearer "+c.getAuth(ctx, token)) c.Client.SetHeader("authorization", "Bearer "+c.getAuth(ctx, token))
resp, err := c.Client.ContentJson().Post(ctx, "https://recharge3.bac365.com/camel_wechat_mini_oil_server/eCardMall/wechatCardGoods", struct { resp, err := c.Client.ContentJson().Post(ctx, "https://recharge3.bac365.com/camel_wechat_mini_oil_server/eCardMall/wechatCardGoods", struct {
Channel string `json:"channel"` Channel string `json:"channel"`
@@ -180,15 +182,17 @@ func (c *Client) QueryAvailableDenominations(ctx context.Context, token string)
Code string `json:"code"` Code string `json:"code"`
Goods []Good `json:"goods"` Goods []Good `json:"goods"`
}{} }{}
if err = json.Unmarshal(resp.ReadAll(), &queryRespStruct); err != nil { respStr := resp.ReadAllString()
glog.Info(ctx, "查询面额", respStr)
if err = json.Unmarshal([]byte(respStr), &queryRespStruct); err != nil {
return nil, err return nil, err
} }
return queryRespStruct.Goods, nil return queryRespStruct.Goods, nil
} }
func (c *Client) CreateOrder(ctx context.Context, phone, token string, amount float64) (orderId string, payUrl string, err error) { func (c *Client) CreateCamelOilOrder(ctx context.Context, phone, token string, amount float64) (orderId string, payUrl string, err error) {
c.Client.SetHeader("Authorization", "Bearer "+c.getAuth(ctx, token)) c.Client.SetHeader("Authorization", "Bearer "+c.getAuth(ctx, token))
goods, err := c.QueryAvailableDenominations(ctx, token) goods, err := c.QueryCamelOilCardAvailableDenominations(ctx, token)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@@ -203,9 +207,18 @@ func (c *Client) CreateOrder(ctx context.Context, phone, token string, amount fl
return "", "", errors.New("当前金额不支持") return "", "", errors.New("当前金额不支持")
} }
// 协程池参数 const maxRetries = 10
const maxConcurrency = 5
const maxRetries = 50 // 获取骆驼模块设置
settingsRes, err := service.CamelOil().GetSettings(ctx, &v1.GetSettingsReq{})
var maxConcurrency int
if err != nil {
glog.Error(ctx, "获取骆驼模块设置失败,使用默认并发数", err)
// 使用默认值继续执行
maxConcurrency = 5
} else {
maxConcurrency = settingsRes.PrefetchConcurrencyAccounts
}
// 结果存储 // 结果存储
var resultMutex sync.Mutex var resultMutex sync.Mutex
@@ -220,7 +233,7 @@ func (c *Client) CreateOrder(ctx context.Context, phone, token string, amount fl
var wg sync.WaitGroup var wg sync.WaitGroup
semaphore := make(chan struct{}, maxConcurrency) semaphore := make(chan struct{}, maxConcurrency)
for i := 0; i < maxRetries; i++ { for range maxRetries {
// 检查是否已经有结果 // 检查是否已经有结果
if completed.Load() { if completed.Load() {
break break
@@ -306,6 +319,10 @@ func (c *Client) CreateOrder(ctx context.Context, phone, token string, amount fl
return return
} }
respStr := resp.ReadAllString() respStr := resp.ReadAllString()
// 记录响应数据到日志
service.CamelOil().SavePrefetchOrderLog(ctx, phone, amount, respStr)
respStruct := struct { respStruct := struct {
Code string `json:"code"` Code string `json:"code"`
Message string `json:"message"` Message string `json:"message"`

View File

@@ -48,7 +48,7 @@ func TestClient_CreateOrder(t *testing.T) {
for t2 := range ticker.C { for t2 := range ticker.C {
glog.Info(t.Context(), t2) glog.Info(t.Context(), t2)
client := NewClient() client := NewClient()
orderId, payUrl, err := client.CreateOrder(t.Context(), "13966750117", "buOSl900L1o6htbHZ6ou32NGtyEsuLu3TeJJlqEZNAvfPzlRk/OqkYm7rMh0X+otku80Jz+sjIlfnf8JXUIjH4NkTRgX92w2knTEjqIc92MSnEi9qyV0lTKue/ycVD1INIGJGBn3vJopJrcb8eupKUjVhFXvONAW2RQ7atAeANc=", 1000) orderId, payUrl, err := client.CreateCamelOilOrder(t.Context(), "13966750117", "buOSl900L1o6htbHZ6ou32NGtyEsuLu3TeJJlqEZNAvfPzlRk/OqkYm7rMh0X+otku80Jz+sjIlfnf8JXUIjH4NkTRgX92w2knTEjqIc92MSnEi9qyV0lTKue/ycVD1INIGJGBn3vJopJrcb8eupKUjVhFXvONAW2RQ7atAeANc=", 100)
if err == nil { if err == nil {
glog.Info(t.Context(), orderId, payUrl, err) glog.Info(t.Context(), orderId, payUrl, err)
break break