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

532 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package camel_oil_api
import (
"context"
"encoding/json"
"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"
"sync"
"sync/atomic"
)
// 绑卡错误类型枚举
const (
RechargeCardSuccess = 0 // 绑卡成功
RechargeCardErrorCode = 1 // 卡密错误
RechargeCardErrorToken = 2 // Token 过期/无效
RechargeCardErrorNetwork = 3 // 网络或其他错误
)
type Client struct {
Client *gclient.Client
}
func NewClient() *Client {
client := gclient.New()
client.SetBrowserMode(true)
client.SetHeaderMap(map[string]string{
"accept-language": "zh-CN,zh-Hans;q=0.9",
"channel": "app",
"Content-Type": "application/json",
"host": "recharge3.bac365.com",
"User-Agent": "Mozilla/5.0 (iPad; CPU OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Html5Plus/1.0 (Immersed/20) uni-app",
})
return &Client{
Client: client,
}
}
func (c *Client) SendCaptcha(ctx context.Context, phone string) (bool, error) {
req := struct {
Phone string `json:"phone"`
Channel string `json:"channel"`
}{
Phone: phone,
Channel: "app",
}
resp, err := c.Client.ContentJson().Post(ctx, "https://recharge3.bac365.com/camel_wechat_mini_oil_server/sendVerifyMessage", req)
if err != nil {
return false, err
}
respStr := resp.ReadAllString()
glog.Info(ctx, "获取信息", respStr)
respStruct := struct {
Code string `json:"code"`
Message string `json:"message"`
}{}
err = json.Unmarshal([]byte(respStr), &respStruct)
return respStruct.Code == "success", err
}
func (c *Client) GetCaptcha(ctx context.Context) (string, error) {
return "eyJjZXJ0aWZ5SWQiOiIxUE5NbzhlTHJGIiwic2NlbmVJZCI6IjFyYW8yZ2w1IiwiaXNTaWduIjp0cnVlLCJzZWN1cml0eVRva2VuIjoiNm9PbzdlNzJuQTYxdVZMaVpWS2lMVHlSNEpNS1pZMng2dGR1bzZqMnRmQmhobkw0TVpNY3lCUUF6eE92NzZLU3pYSTYxQ2pRTmFNTnpvaVVQaEt2dklWNjZxMUJiK3M5ZzVhMU1sZzN5VkdWNUpxNW1rVEdNT3FCazE1SzIzRjQifQ==", nil
//responseStruct := struct {
// RequestId string `json:"RequestId"`
// Message string `json:"Message"`
// HttpStatusCode int `json:"HttpStatusCode"`
// Code string `json:"Code"`
// Success bool `json:"Success"`
// Result struct {
// SecurityToken string `json:"securityToken"`
// VerifyCode string `json:"VerifyCode"`
// VerifyResult bool `json:"VerifyResult"`
// CertifyId string `json:"certifyId"`
// } `json:"Result"`
//}{}
//
//response, err := gclient.New().Get(ctx, "http://124.223.113.140:3012/api_aliv3")
//if err != nil {
// return "", err
//}
//responseStr := response.ReadAllString()
//err = json.Unmarshal([]byte(responseStr), &responseStruct)
//reqSign := struct {
// CertifyId string `json:"certifyId"`
// SceneId string `json:"sceneId"`
// IsSign bool `json:"isSign"`
// SecurityToken string `json:"securityToken"`
//}{
// CertifyId: responseStruct.Result.CertifyId,
// SceneId: "1rao2gl5",
// IsSign: true,
// SecurityToken: responseStruct.Result.SecurityToken,
//}
//signBytes, _ := json.Marshal(reqSign)
//signStr := gbase64.Encode(signBytes)
//return string(signStr), err
}
func (c *Client) LoginWithCaptcha(ctx context.Context, phone string, code string) (string, error) {
//token, err := c.GetCaptcha(ctx)
//if err != nil {
// return "", err
//}
req := struct {
Phone string `json:"phone"`
Codes string `json:"codes"`
Channel string `json:"channel"`
}{
Phone: phone,
Codes: code,
Channel: "app",
}
resp, err := c.Client.ContentJson().Post(ctx, "https://recharge3.bac365.com/camel_wechat_mini_oil_server/loginApp", req)
if err != nil {
return "", err
}
respStr := resp.ReadAllString()
glog.Info(ctx, "登录", req, respStr)
respStruct := struct {
LoginUser struct {
UserIdApp string `json:"userIdApp"`
Phone string `json:"phone"`
UserIdCamel string `json:"userIdCamel"`
LoginTime string `json:"loginTime"`
ExpireTime string `json:"expireTime"`
Ipaddr string `json:"ipaddr"`
} `json:"loginUser,omitempty"`
Code string `json:"code"`
Message string `json:"message"`
Token string `json:"token,omitempty"`
}{}
err = json.Unmarshal([]byte(respStr), &respStruct)
if err != nil {
return "", err
}
if respStruct.Code != "success" {
return "", errors.New(respStruct.Message)
}
return respStruct.Token, err
}
// generateRandomCoordinates 生成指定坐标附近的随机坐标
func generateRandomCoordinates(baseLat, baseLon float64, radius float64) (float64, float64) {
// 在指定半径内生成随机坐标
// 纬度1度约等于111km
latDelta := (rand.Float64()*2 - 1) * (radius / 111000.0)
lonDelta := (rand.Float64()*2 - 1) * (radius / (111000.0 * math.Cos(baseLat*math.Pi/180.0)))
newLat := baseLat + latDelta
newLon := baseLon + lonDelta
return newLat, newLon
}
func (c *Client) getAuth(ctx context.Context, auth string) string {
base64Private := "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIHSkp+Z0Lu+lZWr/wKcMT3EcWEIihKTg/jEOyKaczqG9hWL9UULJ1dFtIQNlpWRySsVZcJLoGTFdGam557lVzpY/tbN73KG9iVMBaKALLF52cgmyg0DRve4atc0OnkhTjv7Rf8B85UokdHCAM/5MgNcjXwqBGHohJ2LGC9yN2erAgMBAAECgYATfTeqww4daTaOkhQF4cnYonl83inQMRoSSe8wuiwLQMCHqounEk4VIW9AlcOh75FaKOuuV+kbx7K6SFskNPy7nGYfS22t2aM9E9Rt+JH+caEniYi5qAfb3gCIgsGExUNI6iuSM2p3/R542EDGc2FyfPPqyht+jR4CjLOLoXHfoQJBALwvF6uIOSW0Lxh7Lo/JsKpWJ1qffDvXWYag605L9JAyP0yO64woF60Tn+mGRzcaEhNDSEjinKQqPEJnxDUGYaECQQCwm1mQKD95MaeKWBiOVJZsdzL5aJsW42xyiu0ZwA7bZUgJyUskzXG0ubeIHK/czlJbev9ODubbMNJFcngX4N3LAkAJaxH0M80oZew1fXTHHYEKBWXS00iUdiK06jjcolCLJvikDEMdsKP+tYy7U00dJODitetYOn88eCCr8iWPwdIBAkBtUjzGt6NS6iHDyXSp5kKXMdIkAVS/flgLL2RFpFWOCcvmAuy5A1N3g97QKrHSBQWGC0UulJri4/3Fb25XmaKxAkBifs9dbUifeqZRNVh2Omck4xedb1FyQPLDicUycjYug3Vca0T/LRr80aX/NhbhtpSdwzF1ukiZ6W46O9DmGuNy" // 私钥 base64 或带 PEM
base64Pub := "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDpwypBPN8r3Zwv3T0XUh1Ka2m2hUe3KBgIyH4fHfN/T1jsBWnbwotKEQdZfRva7mRYiz9YrTHoH/eUAuv+WYqPMubaiqpWOu0l+BzEX1kPGA98qRC06IF2Tk4Z5xAmQ8p8u3O5jxohYFkO2XlDvPU+W9SDZgSEBTe8p80LExgo6wIDAQAB" // 公钥 base64 或带 PEM
authRes, _ := DecryptWithPrivateThenEncryptWithPublic(base64Private, base64Pub, auth)
return authRes
}
// 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"`
}{
Channel: "app",
})
if err != nil {
return nil, err
}
queryRespStruct := struct {
Code string `json:"code"`
Goods []Good `json:"goods"`
}{}
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) 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.QueryCamelOilCardAvailableDenominations(ctx, token)
if err != nil {
return "", "", err
}
goodId := ""
for _, good := range goods {
if good.Denomination == amount {
goodId = good.GoodId
break
}
}
if goodId == "" {
return "", "", errors.New("当前金额不支持")
}
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
var successResult *struct {
orderId string
payUrl string
}
var resultError error
var completed atomic.Bool
// 创建协程池
var wg sync.WaitGroup
semaphore := make(chan struct{}, maxConcurrency)
for range maxRetries {
// 检查是否已经有结果
if completed.Load() {
break
}
wg.Go(func() {
// 获取信号量
semaphore <- struct{}{}
defer func() { <-semaphore }()
// 已有结果则跳过
if completed.Load() {
return
}
//captchaSign, err2 := c.GetCaptcha(ctx)
//if err2 != nil {
// return
//}
// 生成随机坐标在原坐标周围1公里范围内
paramY, paramX := generateRandomCoordinates(36.36142896926231, 118.9886283180532, 1000.0)
bodyStr := struct {
OpenId string `json:"openId"`
Phone string `json:"phone"`
GoodId string `json:"goodId"`
GoodNum int `json:"goodNum"`
BindPhone string `json:"bindPhone"`
PayType string `json:"payType"`
ParamY float64 `json:"paramY"`
ParamX float64 `json:"paramX"`
Yanqian bool `json:"yanqian"`
MobileOperatingPlatform string `json:"mobileOperatingPlatform"`
SysVersion string `json:"sysVersion"`
PlatformType string `json:"platformType"`
NetWork string `json:"netWork"`
Platform string `json:"platform"`
Brand string `json:"brand"`
DeviceId string `json:"deviceId"`
}{
OpenId: "app2511282235395452908",
Phone: phone,
GoodId: goodId,
GoodNum: 1,
BindPhone: phone,
PayType: "appAli",
ParamY: paramY,
ParamX: paramX,
Yanqian: true,
MobileOperatingPlatform: "ios",
SysVersion: "iOS 26.2",
PlatformType: "iPad Pro (12.9-inch) (3rd generation)",
NetWork: "unknown",
Platform: "ios",
Brand: "apple",
DeviceId: "4E071A21C568F1939D61F38E5F5C3813",
}
pubkey := "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCkc6Xr/JhWEx/WPxG2q3VHLQ+FYk/oCmQ1y14B5j4xOJY+mAWoDOOti3sAXg0Kk662gWjWET1nLI6YED4wb9HWon1NAZn47lgc5ohIpEdU91Jao85X/kgkD3NvTTvhFicttepUOsrYUZN8rAQCE7AhzwGgKnCiIRY/kE8jOCCeZQIDAQAB"
body, _ := json.Marshal(&bodyStr)
bodyS, _ := EncryptWithPublicKey(pubkey, string(body))
req := struct {
BodyStr string `json:"bodyStr"`
Yanqian bool `json:"yanqian"`
Channel string `json:"channel"`
}{
BodyStr: bodyS,
Channel: "app",
Yanqian: true,
}
//proxy, err2 := service.ProxyPool().GetProxyByOrderId(ctx, phone)
//if err2 == nil && proxy.Host != "" {
// glog.Info(ctx, "代理 ip", proxy.String())
// c.Client.SetProxy(proxy.String())
//}
resp, err1 := c.Client.Post(ctx, "https://recharge3.bac365.com/camel_wechat_mini_oil_server/eCardMall/wechatCardOrder", req)
if err1 != nil {
resultMutex.Lock()
if !completed.Load() && resultError == nil {
resultError = err1
}
resultMutex.Unlock()
return
}
respStr := resp.ReadAllString()
// 记录响应数据到日志
service.CamelOil().SavePrefetchOrderLog(ctx, phone, amount, respStr)
respStruct := struct {
Code string `json:"code"`
Message string `json:"message"`
OrderRes struct {
Body string `json:"body"`
} `json:"orderRes,omitempty"`
OrderId string `json:"orderid,omitempty"`
}{}
err = json.Unmarshal([]byte(respStr), &respStruct)
if err != nil {
resultMutex.Lock()
if !completed.Load() && resultError == nil {
resultError = err
}
resultMutex.Unlock()
return
}
// 处理响应
switch respStruct.Code {
case "limit", "error":
if respStruct.Code == "error" && strings.Contains(respStruct.Message, "系统繁忙") {
return // 继续重试
}
return
case "auth_error":
resultMutex.Lock()
if !completed.Load() {
resultError = errors.New("auth_error")
completed.Store(true)
}
resultMutex.Unlock()
return
case "success":
base64PrivateKey := "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKRzpev8mFYTH9Y/EbardUctD4ViT+gKZDXLXgHmPjE4lj6YBagM462LewBeDQqTrraBaNYRPWcsjpgQPjBv0daifU0BmfjuWBzmiEikR1T3Ulqjzlf+SCQPc29NO+EWJy216lQ6ythRk3ysBAITsCHPAaAqcKIhFj+QTyM4IJ5lAgMBAAECgYEAg/vlMI7b3EkhBhw8JTVavLMnf8+1fe/JGXuMiU22oF5gBwCPmZ4upLwLDfJt2Q1J7WPTNetEMqgKEXUH1GwKJkFGm2tSEMHSHdTmUTQ3w6bS1C0peZghyhmlWRXUlpKk5tDOQ24sWO268YrwZyueXnVGKJ4s0hY0KOiZIU2trUECQQDxA+lzq/t/L09M/bUybjsiP6eb/HBeZeu+2+JnHekb8z9BMXTOKTHqAI0Cs9UvE6BDT3aU9IJbWHbRogIMypT9AkEArq0fccphwWtAIyS0+fns4Hqs4On7yTfXSXWiAbSVif1LxP60b5n5Xm8lo12oHkdwOvKaesvgDpnIGUM9xjFfiQJASTZrABxKNYRljnmzRTJ+/BRiEdxJNiO3zS52Q+SuHzNxD5i6ZrXU18R7EUsXg0lu8YN9/hmYT687yMpx3Pjc8QJAZBs1lSouQgIsPLfRvB1+otvLbg7KzPPivufak+2hcfanUNvEHt14a6V5RZnsOoYojK/y1oM3AkchxVCi+43aOQJAC0gI6qsZ3VaPu9QDddrHPJ1dCHTXyfcNJ0op3srCVF92HoBWX54pzeagj+9g/Z4oUT9IhaO0Q3YE07N03HuVrQ=="
respData, _ := DecryptWithPrivateKey(base64PrivateKey, respStruct.OrderRes.Body)
resultMutex.Lock()
if !completed.Load() {
successResult = &struct {
orderId string
payUrl string
}{
orderId: respStruct.OrderId,
payUrl: respData,
}
completed.Store(true)
}
resultMutex.Unlock()
return
}
})
}
wg.Wait()
// 返回结果
if successResult != nil {
return successResult.orderId, successResult.payUrl, nil
}
if resultError != nil {
return "", "", resultError
}
return "", "", errors.New("创建订单超时")
}
// QueryOrder 查询对应订单
// 返回值说明:
// - result != nil && err == nil: 查询成功,找到订单
// - result == nil && err == nil: 查询成功,但未找到订单
// - result == nil && err != nil: 查询失败,有错误
func (c *Client) QueryOrder(ctx context.Context, phone, token, orderId string) (result *QueryResult, err error) {
c.Client.SetHeader("Authorization", "Bearer "+c.getAuth(ctx, token))
// 最多查询100页防止无限循环
maxPages := 100
pageNum := 1
for pageNum <= maxPages {
reqBody := struct {
OrderId string `json:"orderId"`
Sign string `json:"sign"`
Channel string `json:"channel"`
Status string `json:"status"`
PageNum int `json:"pageNum"`
PageSize int `json:"pageSize"`
OpenId string `json:"openId"`
}{
OrderId: orderId,
Sign: Sign("app2511181557205741495"),
Channel: "app",
Status: "unused",
PageNum: pageNum,
PageSize: 50,
OpenId: "app2511181557205741495",
}
respData, err := c.Client.Post(ctx, "https://recharge3.bac365.com/camel_wechat_mini_oil_server/eCardMall/queryWechatUserECards", reqBody)
if err != nil {
glog.Errorf(ctx, "查询卡券失败,第%d页错误: %v", pageNum, err)
return nil, err
}
respStruct := struct {
Total string `json:"total"`
Code string `json:"code"`
Cards []struct {
RecordId string `json:"recordId"`
Denomination float64 `json:"denomination"`
CreateTime string `json:"createTime"`
ExpireTime string `json:"expireTime"`
Status string `json:"status"`
OrderId string `json:"orderId"`
ECardNo string `json:"ecardNo"`
ECardCode string `json:"ecardCode"`
} `json:"cards,omitempty"`
}{}
err = json.Unmarshal(respData.ReadAll(), &respStruct)
if err != nil {
glog.Errorf(ctx, "解析响应失败,第%d页错误: %v", pageNum, err)
return nil, err
}
// 如果当前页没有卡券数据,说明已查询所有数据
if len(respStruct.Cards) == 0 {
glog.Debugf(ctx, "查询订单%s已完成共查询%d页未找到", orderId, pageNum-1)
return nil, nil
}
// 在当前页查找目标订单
for _, card := range respStruct.Cards {
if card.OrderId == orderId {
glog.Infof(ctx, "查询订单%s成功金额: %.2f", orderId, card.Denomination)
return &QueryResult{
Balance: card.Denomination,
CardNumber: card.ECardNo,
CardPassword: card.ECardCode,
}, nil
}
}
// 当前页未找到,继续查询下一页
pageNum++
}
glog.Warningf(ctx, "查询订单%s超过最大页数%d未找到", orderId, maxPages)
return nil, nil
}
// RechargeCard 绑卡接口
// 返回值说明:
// - errType == RechargeCardSuccess: 绑卡成功
// - errType == RechargeCardErrorCode: 卡密错误
// - errType == RechargeCardErrorToken: token 过期/无效
// - errType == RechargeCardErrorNetwork: 网络或其他错误
func (c *Client) RechargeCard(ctx context.Context, token, phone, eCardCode string) (errType int, err error) {
c.Client.SetHeader("Authorization", "Bearer "+c.getAuth(ctx, token))
req := struct {
ECardCode string `json:"eCardCode"`
OpenId string `json:"openId"`
Phone string `json:"phone"`
Channel string `json:"channel"`
}{
ECardCode: eCardCode,
OpenId: "app2511181557205741495",
Phone: phone,
Channel: "app",
}
resp, err := c.Client.ContentJson().Post(ctx, "https://recharge3.bac365.com/camel_wechat_mini_oil_server/eCardMall/eCardRecharge", req)
if err != nil {
glog.Errorf(ctx, "绑卡请求失败,错误: %v", err)
return RechargeCardErrorNetwork, err
}
respStruct := struct {
Code string `json:"code"`
Message string `json:"message"`
}{}
err = json.Unmarshal(resp.ReadAll(), &respStruct)
if err != nil {
glog.Errorf(ctx, "解析绑卡响应失败,错误: %v", err)
return RechargeCardErrorNetwork, err
}
// 根据不同的错误码进行分类
switch respStruct.Code {
case "success":
glog.Infof(ctx, "卡密绑卡成功,手机号: %s", phone)
return RechargeCardSuccess, nil
case "codeError":
err = errors.New(respStruct.Message)
glog.Warningf(ctx, "卡密错误: %v", err)
return RechargeCardErrorCode, err
case "auth_error", "unauthorized":
err = errors.New(respStruct.Message)
glog.Warningf(ctx, "Token 错误或已过期: %v", err)
return RechargeCardErrorToken, err
default:
err = errors.New(respStruct.Message)
glog.Errorf(ctx, "绑卡失败: %v", err)
return RechargeCardErrorNetwork, err
}
}