Files
kami_backend/utility/integration/camel_oil_api/api.go
danial 3588bf9af6 feat(camel_oil): 支持Token管理与卡密绑定功能
- 新增CamelOilToken和CamelOilCardBinding数据库表,实现Token及卡密绑定记录管理
- 在service层增加Token的创建、查询、更新、删除及分页功能
- 实现卡密与Token绑定的业务逻辑,支持基于Token的卡密管理
- 在API层新增Token和卡密绑定相关接口:创建Token、获取Token详情、删除Token、列出Token及根据Token查询绑定卡密
- camel_oil_api新增绑卡接口,支持绑卡状态分类及错误处理
- 在定时任务中增加卡密绑定任务,实现自动处理已支付订单的卡密绑定
- 优化订单提交及支付流程,包含日志调整和请求参数随机扰动
- 统一调整camel_oil模块多控制器实现,完成账号状态查询及订单相关接口实现
- 注册更多camel_oil定时任务,包括订单支付检查、账号日重置和待回调订单处理任务
2025-11-23 00:08:35 +08:00

362 lines
14 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"
"math/rand/v2"
"strings"
"github.com/gogf/gf/v2/net/gclient"
"github.com/gogf/gf/v2/os/glog"
"github.com/google/uuid"
)
// 绑卡错误类型枚举
const (
RechargeCardSuccess = 0 // 绑卡成功
RechargeCardErrorCode = 1 // 卡密错误
RechargeCardErrorToken = 2 // Token 过期/无效
RechargeCardErrorNetwork = 3 // 网络或其他错误
)
type Client struct {
Client *gclient.Client
}
func NewClient() *Client {
return &Client{
Client: gclient.New(),
}
}
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) LoginWithCaptcha(ctx context.Context, phone string, code string) (string, error) {
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
}
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
}
func (c *Client) CreateOrder(ctx context.Context, phone, token string, amount float64) (orderId string, payUrl string, err 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 "", "", err
}
queryRespStruct := struct {
Code string `json:"code"`
Goods []struct {
GoodId string `json:"goodId"`
GoodName string `json:"goodName"`
Denomination float64 `json:"denomination"`
GoodPrice float64 `json:"goodPrice"`
Validity int `json:"validity"`
ImgUrl string `json:"imgUrl"`
Status string `json:"status"`
} `json:"goods"`
}{}
if err = json.Unmarshal(resp.ReadAll(), &queryRespStruct); err != nil {
return "", "", err
}
goodId := ""
for _, good := range queryRespStruct.Goods {
if good.Denomination == amount {
goodId = good.GoodId
break
}
}
if goodId == "" {
return "", "", errors.New("当前金额不支持")
}
for range 100 {
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: "app2511221747117503039",
Phone: phone,
GoodId: goodId,
GoodNum: 1,
BindPhone: phone,
PayType: "appAli",
ParamY: 31.2304 + (rand.Float64()-0.5)*2,
ParamX: 121.4737 + (rand.Float64()-0.5)*2,
Yanqian: true,
MobileOperatingPlatform: "iOS",
SysVersion: "iOS 15.7",
PlatformType: "iPad Pro (12.9-inch) (3rd generation)",
NetWork: "unknown",
Platform: "ios",
Brand: "apple",
DeviceId: strings.ToUpper(strings.ReplaceAll(uuid.NewString(), "-", "")),
}
pubkey := "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCkc6Xr/JhWEx/WPxG2q3VHLQ+FYk/oCmQ1y14B5j4xOJY+mAWoDOOti3sAXg0Kk662gWjWET1nLI6YED4wb9HWon1NAZn47lgc5ohIpEdU91Jao85X/kgkD3NvTTvhFicttepUOsrYUZN8rAQCE7AhzwGgKnCiIRY/kE8jOCCeZQIDAQAB"
body, _ := json.Marshal(&bodyStr)
bodyS, _ := EncryptWithPublicKey(pubkey, string(body))
req := struct {
BodyStr string `json:"bodyStr"`
Channel string `json:"channel"`
Yanqian bool `json:"yanqian"`
}{
BodyStr: bodyS,
Channel: "app",
Yanqian: true,
}
resp, err = c.Client.ContentJson().Post(ctx, "https://recharge3.bac365.com/camel_wechat_mini_oil_server/eCardMall/createShoppingOrder", req)
if err != nil {
return "", "", err
}
respStr := resp.ReadAllString()
glog.Info(ctx, "登录", bodyStr, 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 {
return "", "", err
}
if respStruct.Code == "limit" {
continue
}
if respStruct.Code == "auth_error" {
return "", "", errors.New("auth_error")
}
if respStruct.Code == "error" && strings.Contains(respStruct.Message, "系统繁忙") {
continue
}
if respStruct.Code == "success" {
base64PrivateKey := "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKRzpev8mFYTH9Y/EbardUctD4ViT+gKZDXLXgHmPjE4lj6YBagM462LewBeDqTrraBaNYRPWcsjpgQPjBv0daifU0BmfjuWBzmiEikR1T3Ulqjzlf+SCQPc29NO+EWJy216l6ythRk3ysBAITsCHPAaAqcKIhFj+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)
return respStruct.OrderId, respData, err
}
}
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))
// 最多查询10页防止无限循环
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
}
}