feat(camel_oil): 添加骆驼模块设置和预拉取订单日志功能
- 增加骆驼模块设置接口支持获取和更新配置 - 使用Redis缓存设置数据,实现模块配置的持久化管理 - 引入预拉取订单日志功能,支持日志的保存和按时间范围查询 - 预拉取订单请求响应数据记录到Redis,方便问题追踪 - 根据模块设置动态调整账号登录、预拉取订单并发数量 - 调整账号登录逻辑以支持配置的并发控制 - 优化预拉取订单补充流程,支持多面额库存管理 - 修正集成API请求函数名及调用,记录详细调用日志数据 - 调整定时任务调度频率,增加预拉取订单补充任务的执行频率 - 升级golang版本到1.25.5,保持开发环境最新状态
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
golang 1.25.3
|
||||
golang 1.25.5
|
||||
python 3.13.9
|
||||
|
||||
@@ -21,6 +21,8 @@ type ICamelOilV1 interface {
|
||||
OrderHistory(ctx context.Context, req *v1.OrderHistoryReq) (res *v1.OrderHistoryRes, 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)
|
||||
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)
|
||||
GetToken(ctx context.Context, req *v1.GetTokenReq) (res *v1.GetTokenRes, err error)
|
||||
ListTokens(ctx context.Context, req *v1.ListTokensReq) (res *v1.ListTokensRes, err error)
|
||||
|
||||
33
api/camel_oil/v1/order_logs.go
Normal file
33
api/camel_oil/v1/order_logs.go
Normal 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:"预拉取订单日志列表"`
|
||||
}
|
||||
50
api/camel_oil/v1/settings.go
Normal file
50
api/camel_oil/v1/settings.go
Normal 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:"要获取的面额和对应库存设置列表"`
|
||||
}
|
||||
@@ -210,18 +210,9 @@ var CamelOilPrefetchOrderChangeTypeText = map[CamelOilPrefetchOrderChangeType]st
|
||||
// ====================================================================================
|
||||
|
||||
const (
|
||||
// CamelOilPrefetchOrderMinCapacity 预拉取订单最小库存阈值(当库存低于此值时触发补充)
|
||||
CamelOilPrefetchOrderMinCapacity = 1
|
||||
|
||||
// CamelOilPrefetchOrderTargetCapacity 预拉取订单目标库存(补充时的目标数量)
|
||||
CamelOilPrefetchOrderTargetCapacity = 5
|
||||
|
||||
// CamelOilPrefetchOrderExpireDuration 预拉取订单过期时间(小时)
|
||||
CamelOilPrefetchOrderExpireDuration = time.Hour * 24
|
||||
|
||||
// CamelOilPrefetchMaxConcurrency 预拉取最大并发账号数量
|
||||
CamelOilPrefetchMaxConcurrency = 10
|
||||
|
||||
// CamelOilPrefetchOrderLockKey Redis中预拉取订单的分布式锁键名前缀
|
||||
CamelOilPrefetchOrderLockKey = "camel_oil_api:prefetch:order:lock:"
|
||||
|
||||
@@ -237,18 +228,12 @@ const (
|
||||
// CamelOilAccountDailyOrderLimit 账号每日订单上限
|
||||
CamelOilAccountDailyOrderLimit = 10
|
||||
|
||||
// CamelOilTargetOnlineAccounts 目标在线账号数量
|
||||
CamelOilTargetOnlineAccounts = 10
|
||||
|
||||
// CamelOilOrderExpireDuration 订单支付超时时间(小时)
|
||||
CamelOilOrderExpireDuration = gtime.H
|
||||
|
||||
// CamelOilMaxCallbackRetry 回调最大重试次数
|
||||
CamelOilMaxCallbackRetry = 3
|
||||
|
||||
// CamelOilMaxLoginConcurrency 最大并发登录数量
|
||||
CamelOilMaxLoginConcurrency = 3
|
||||
|
||||
// CamelOilTokenExpireDuration Token过期时间(天)
|
||||
CamelOilTokenExpireDuration = 30
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
12
internal/controller/camel_oil/camel_oil_v1_get_settings.go
Normal file
12
internal/controller/camel_oil/camel_oil_v1_get_settings.go
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"kami/utility/config"
|
||||
"kami/utility/integration/camel_oil_api"
|
||||
"kami/utility/integration/pig"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -19,13 +20,25 @@ import (
|
||||
// LoginAccount 执行账号登录流程
|
||||
// 注意:当前使用假数据,实际应对接骆驼加油平台和接码平台
|
||||
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
|
||||
ticker := time.NewTicker(time.Second)
|
||||
for range ticker.C {
|
||||
phoneNumber, err = pig.NewClient().GetAccountInfo(ctx)
|
||||
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")
|
||||
}
|
||||
|
||||
// 逐个登录账号
|
||||
successCount = 0
|
||||
for range 10 {
|
||||
if successCount >= count {
|
||||
break
|
||||
// 获取设置
|
||||
settings, err := GetCamelOilSettings(ctx)
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "获取骆驼模块设置失败: %v", err)
|
||||
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)
|
||||
if loginErr != nil {
|
||||
glog.Errorf(ctx, "账号登录失败,错误: %v", loginErr)
|
||||
return
|
||||
}
|
||||
atomic.AddInt64(&successCount, 1)
|
||||
}
|
||||
atomic.AddInt64(&successCounter, 1)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
successCount = successCounter
|
||||
glog.Infof(ctx, "批量登录完成,成功: %d", successCount)
|
||||
return successCount, nil
|
||||
}
|
||||
|
||||
@@ -26,6 +26,13 @@ func (s *sCamelOil) CronAccountPrefetchTask(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取设置
|
||||
settings, err := GetCamelOilSettings(ctx)
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "获取骆驼模块设置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 1. 获取当前在线账号数量
|
||||
m := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1())
|
||||
onlineCount, err := m.Where(dao.V1CamelOilAccount.Columns().Status, consts.CamelOilAccountStatusOnline).
|
||||
@@ -36,11 +43,12 @@ func (s *sCamelOil) CronAccountPrefetchTask(ctx context.Context) error {
|
||||
onlineCount = 0
|
||||
}
|
||||
|
||||
glog.Infof(ctx, "当前在线账号数量: %d, 目标数量: %d", onlineCount, consts.CamelOilTargetOnlineAccounts)
|
||||
targetOnlineAccounts := settings.LoginAccountCount
|
||||
glog.Infof(ctx, "当前在线账号数量: %d, 目标数量: %d", onlineCount, targetOnlineAccounts)
|
||||
|
||||
// 2. 如果在线账号少于目标数,触发并发登录
|
||||
if onlineCount < consts.CamelOilTargetOnlineAccounts {
|
||||
needCount := consts.CamelOilTargetOnlineAccounts - onlineCount
|
||||
if onlineCount < targetOnlineAccounts {
|
||||
needCount := targetOnlineAccounts - onlineCount
|
||||
glog.Infof(ctx, "在线账号不足,需要登录 %d 个账号", needCount)
|
||||
|
||||
// 使用并发登录提高效率
|
||||
|
||||
@@ -44,6 +44,13 @@ func (s *sCamelOil) GetPrefetchOrderCapacity(ctx context.Context, amount float64
|
||||
|
||||
// PrefetchOrderConcurrently 使用所有可用账号并发拉取订单,直到获取到可用订单为止
|
||||
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. 获取所有在线账号
|
||||
m := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1())
|
||||
var onlineAccounts []*entity.V1CamelOilAccount
|
||||
@@ -59,8 +66,8 @@ func (s *sCamelOil) PrefetchOrderConcurrently(ctx context.Context, amount float6
|
||||
return nil, gerror.New("暂无在线账号可用")
|
||||
}
|
||||
|
||||
// 2. 使用控制并发量的信信道控制并发
|
||||
concurrencyLimit := min(len(onlineAccounts), consts.CamelOilPrefetchMaxConcurrency)
|
||||
// 2. 使用设置中的并发数量限制并发
|
||||
concurrencyLimit := min(len(onlineAccounts), settings.PrefetchConcurrencyAccounts)
|
||||
|
||||
var (
|
||||
resultChan = make(chan *model.PrefetchOrderResult, 1)
|
||||
@@ -88,7 +95,7 @@ func (s *sCamelOil) PrefetchOrderConcurrently(ctx context.Context, amount float6
|
||||
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.Error() == "auth_error" {
|
||||
_ = s.UpdateAccountStatus(ctx, acc.Id, consts.CamelOilAccountStatusInvalid, consts.CamelOilAccountChangeTypeInvalidate, "账号token失效")
|
||||
@@ -138,7 +145,7 @@ func (s *sCamelOil) PrefetchOrderConcurrently(ctx context.Context, amount float6
|
||||
// PrefetchOrder 拉取单个订单(用于单个账号)
|
||||
func (s *sCamelOil) PrefetchOrder(ctx context.Context, account *entity.V1CamelOilAccount, amount float64) (prefetchId int64, err error) {
|
||||
// 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.Error() == "auth_error" {
|
||||
_ = 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 使用多个账号并发拉取订单
|
||||
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. 获取所有在线账号
|
||||
m := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1())
|
||||
var onlineAccounts []*entity.V1CamelOilAccount
|
||||
@@ -193,11 +207,8 @@ func (s *sCamelOil) ConcurrentPrefetchOrders(ctx context.Context, amount float64
|
||||
return 0, gerror.New("暂无在线账号可用于拉取订单")
|
||||
}
|
||||
|
||||
// 2. 使用协程池并发拉取
|
||||
concurrencyLimit := consts.CamelOilPrefetchMaxConcurrency
|
||||
if len(onlineAccounts) < concurrencyLimit {
|
||||
concurrencyLimit = len(onlineAccounts)
|
||||
}
|
||||
// 2. 使用设置中的并发数量限制并发
|
||||
concurrencyLimit := settings.PrefetchConcurrencyAccounts
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
@@ -274,42 +285,46 @@ func (s *sCamelOil) SupplementPrefetchOrders(ctx context.Context) (supplementedC
|
||||
gmlock.Lock(consts.CamelOilPrefetchTaskLockKey)
|
||||
defer gmlock.Unlock(consts.CamelOilPrefetchTaskLockKey)
|
||||
|
||||
//找到一个可用账户
|
||||
account := &entity.V1CamelOilAccount{}
|
||||
_ = 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)
|
||||
// 获取设置
|
||||
settings, err := GetCamelOilSettings(ctx)
|
||||
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
|
||||
for _, good := range goods {
|
||||
for _, denom := range settings.TargetDenominations {
|
||||
// 1. 获取当前库存
|
||||
capacity, err2 := s.GetPrefetchOrderCapacity(ctx, good.GoodPrice)
|
||||
capacity, err2 := s.GetPrefetchOrderCapacity(ctx, float64(denom.Denomination))
|
||||
if err2 != nil {
|
||||
return 0, gerror.Wrap(err2, "获取预拉取订单库存失败")
|
||||
}
|
||||
|
||||
glog.Infof(ctx, "当前预拉取订单库存: %d", capacity)
|
||||
glog.Infof(ctx, "当前预拉取订单库存 (面额 %d): %d", denom.Denomination, capacity)
|
||||
|
||||
// 2. 如果库存充足,无需补充
|
||||
if capacity >= consts.CamelOilPrefetchOrderMinCapacity {
|
||||
glog.Infof(ctx, "预拉取订单库存充足 (%d >= %d),无需补充", capacity, consts.CamelOilPrefetchOrderMinCapacity)
|
||||
return 0, nil
|
||||
if capacity >= denom.MinCapacity {
|
||||
glog.Infof(ctx, "面额 %d 预拉取订单库存充足 (%d >= %d),无需补充", denom.Denomination, capacity, denom.MinCapacity)
|
||||
continue
|
||||
}
|
||||
|
||||
// 3. 计算需要补充的数量
|
||||
needCount := consts.CamelOilPrefetchOrderTargetCapacity - capacity
|
||||
glog.Infof(ctx, "预拉取订单库存不足,需要补充 %d 单,金额: 100元", needCount)
|
||||
needCount := denom.TargetCapacity - capacity
|
||||
glog.Infof(ctx, "面额 %d 预拉取订单库存不足,需要补充 %d 单", denom.Denomination, needCount)
|
||||
|
||||
// 4. 并发拉取订单
|
||||
successCount, err = s.ConcurrentPrefetchOrders(ctx, good.GoodPrice, needCount)
|
||||
success, err := s.ConcurrentPrefetchOrders(ctx, float64(denom.Denomination), needCount)
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "并发拉取订单失败")
|
||||
glog.Errorf(ctx, "面额 %d 并发拉取订单失败: %v", denom.Denomination, err)
|
||||
continue
|
||||
}
|
||||
successCount += success
|
||||
}
|
||||
return successCount, nil
|
||||
}
|
||||
|
||||
165
internal/logic/camel_oil/prefetch_order_logs.go
Normal file
165
internal/logic/camel_oil/prefetch_order_logs.go
Normal 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)
|
||||
}
|
||||
97
internal/logic/camel_oil/settings.go
Normal file
97
internal/logic/camel_oil/settings.go
Normal 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
|
||||
}
|
||||
@@ -105,6 +105,14 @@ type (
|
||||
SupplementPrefetchOrders(ctx context.Context) (supplementedCount int, err error)
|
||||
// MatchPrefetchOrder 将预拉取订单与用户订单进行匹配
|
||||
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(ctx context.Context, tokenName string, tokenValue string, phone string, remark string, rechargeLimitAmount float64, rechargeLimitCount int) (tokenId int64, err error)
|
||||
// GetTokenInfo 获取 Token 信息
|
||||
|
||||
1
utility/cache/consts.go
vendored
1
utility/cache/consts.go
vendored
@@ -18,6 +18,7 @@ const (
|
||||
RedeemAccountTargetIDByUser CachedEnum = "redeem_account_target_id_by_user"
|
||||
RedeemAccountTargetIDByCKAndUser CachedEnum = "redeem_account_target_account_id_by_ck_and_user"
|
||||
RedeemAccountTmpStopped CachedEnum = "redeem_account_tmp_stopped"
|
||||
CamelOilSettings CachedEnum = "camel_oil_settings"
|
||||
)
|
||||
|
||||
func (e CachedEnum) Key(key interface{}) string {
|
||||
|
||||
@@ -97,9 +97,9 @@ func registerCamelOilTasks(ctx context.Context) {
|
||||
_ = service.CamelOil().CronAccountDailyResetTask(ctx)
|
||||
}, "CamelOilAccountDailyReset")
|
||||
|
||||
_, _ = gcron.AddSingleton(ctx, "@every 60m", func(ctx context.Context) {
|
||||
_, _ = service.CamelOil().CronCleanExpiredPrefetchOrders(ctx)
|
||||
}, "CamelOilCleanExpiredPrefetchOrders")
|
||||
//_, _ = gcron.AddSingleton(ctx, "@every 60m", func(ctx context.Context) {
|
||||
// _, _ = service.CamelOil().CronCleanExpiredPrefetchOrders(ctx)
|
||||
//}, "CamelOilCleanExpiredPrefetchOrders")
|
||||
|
||||
_, _ = gcron.AddSingleton(ctx, "@every 1s", func(ctx context.Context) {
|
||||
_ = service.CamelOil().CronPrefetchOrderSupplementTask(ctx)
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"errors"
|
||||
"github.com/gogf/gf/v2/net/gclient"
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
"kami/api/camel_oil/v1"
|
||||
"kami/internal/service"
|
||||
"math"
|
||||
"math/rand"
|
||||
"strings"
|
||||
@@ -165,8 +167,8 @@ func (c *Client) getAuth(ctx context.Context, auth string) string {
|
||||
return authRes
|
||||
}
|
||||
|
||||
// QueryAvailableDenominations 查询所有可用面额
|
||||
func (c *Client) QueryAvailableDenominations(ctx context.Context, token string) ([]Good, error) {
|
||||
// QueryCamelOilCardAvailableDenominations 查询所有可用面额
|
||||
func (c *Client) QueryCamelOilCardAvailableDenominations(ctx context.Context, token string) ([]Good, error) {
|
||||
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 {
|
||||
Channel string `json:"channel"`
|
||||
@@ -180,15 +182,17 @@ func (c *Client) QueryAvailableDenominations(ctx context.Context, token string)
|
||||
Code string `json:"code"`
|
||||
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 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))
|
||||
goods, err := c.QueryAvailableDenominations(ctx, token)
|
||||
goods, err := c.QueryCamelOilCardAvailableDenominations(ctx, token)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -203,9 +207,18 @@ func (c *Client) CreateOrder(ctx context.Context, phone, token string, amount fl
|
||||
return "", "", errors.New("当前金额不支持")
|
||||
}
|
||||
|
||||
// 协程池参数
|
||||
const maxConcurrency = 5
|
||||
const maxRetries = 50
|
||||
const maxRetries = 10
|
||||
|
||||
// 获取骆驼模块设置
|
||||
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
|
||||
@@ -220,7 +233,7 @@ func (c *Client) CreateOrder(ctx context.Context, phone, token string, amount fl
|
||||
var wg sync.WaitGroup
|
||||
semaphore := make(chan struct{}, maxConcurrency)
|
||||
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
for range maxRetries {
|
||||
// 检查是否已经有结果
|
||||
if completed.Load() {
|
||||
break
|
||||
@@ -306,6 +319,10 @@ func (c *Client) CreateOrder(ctx context.Context, phone, token string, amount fl
|
||||
return
|
||||
}
|
||||
respStr := resp.ReadAllString()
|
||||
|
||||
// 记录响应数据到日志
|
||||
service.CamelOil().SavePrefetchOrderLog(ctx, phone, amount, respStr)
|
||||
|
||||
respStruct := struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
|
||||
@@ -48,7 +48,7 @@ func TestClient_CreateOrder(t *testing.T) {
|
||||
for t2 := range ticker.C {
|
||||
glog.Info(t.Context(), t2)
|
||||
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 {
|
||||
glog.Info(t.Context(), orderId, payUrl, err)
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user