refactor(camel_oil): 优化账号登录逻辑及预拉取订单接口

- 删除旧的容量检查并触发账号登录代码,统一登录流程
- LoginAccount方法支持手机号去重,避免重复创建账号
- BatchLoginAccounts改用并发登录提高效率,支持int64数量参数
- camel_oil_api集成更新,调整接口调用地址和请求体,新增QueryOrder接口实现分页查询
- pig集成重试获取账号,增强鲁棒性
- 更新consts增加预拉取订单相关状态和类型常量及文本映射
- 服务接口新增预拉取订单相关方法和补充任务调度接口
- 调整部分测试代码,注释无效测试
- 代码格式和日志输出格式优化,增强可读性和维护性
This commit is contained in:
danial
2025-11-22 19:16:46 +08:00
parent 80f605877f
commit 0f19ea2a33
52 changed files with 2326 additions and 426 deletions

3
go.mod
View File

@@ -1,6 +1,6 @@
module kami
go 1.24.0
go 1.25
require (
github.com/Lofanmi/pinyin-golang v0.0.0-20250305082105-87d20ae3d695
@@ -33,7 +33,6 @@ require (
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
golang.org/x/image v0.32.0
golang.org/x/net v0.46.0
golang.org/x/time v0.14.0
)
require (

2
go.sum
View File

@@ -274,8 +274,6 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

View File

@@ -125,6 +125,7 @@ const (
CamelOilOrderChangeTypeFail CamelOilOrderChangeType = "fail" // 下单失败
CamelOilOrderChangeTypeCallbackSuccess CamelOilOrderChangeType = "callback_success" // 回调商户成功
CamelOilOrderChangeTypeCallbackFail CamelOilOrderChangeType = "callback_fail" // 回调商户失败
CamelOilOrderChangeTypeFillCard CamelOilOrderChangeType = "fill_card" // 填写卡密和卡号
)
// CamelOilOrderChangeTypeText 订单变更类型文本映射
@@ -138,8 +139,75 @@ var CamelOilOrderChangeTypeText = map[CamelOilOrderChangeType]string{
CamelOilOrderChangeTypeFail: "下单失败",
CamelOilOrderChangeTypeCallbackSuccess: "回调成功",
CamelOilOrderChangeTypeCallbackFail: "回调失败",
CamelOilOrderChangeTypeFillCard: "填写卡密",
}
// ====================================================================================
// 预拉取订单相关常量定义
// ====================================================================================
// CamelOilPrefetchOrderStatus 预拉取订单状态枚举
type CamelOilPrefetchOrderStatus int
const (
CamelOilPrefetchOrderStatusPending CamelOilPrefetchOrderStatus = 1 // 待匹配
CamelOilPrefetchOrderStatusMatched CamelOilPrefetchOrderStatus = 2 // 已匹配
CamelOilPrefetchOrderStatusExpired CamelOilPrefetchOrderStatus = 3 // 已过期
CamelOilPrefetchOrderStatusInvalid CamelOilPrefetchOrderStatus = 4 // 已失效
)
// CamelOilPrefetchOrderStatusText 预拉取订单状态文本映射
var CamelOilPrefetchOrderStatusText = map[CamelOilPrefetchOrderStatus]string{
CamelOilPrefetchOrderStatusPending: "待匹配",
CamelOilPrefetchOrderStatusMatched: "已匹配",
CamelOilPrefetchOrderStatusExpired: "已过期",
CamelOilPrefetchOrderStatusInvalid: "已失效",
}
// CamelOilPrefetchOrderChangeType 预拉取订单变更类型
type CamelOilPrefetchOrderChangeType string
const (
CamelOilPrefetchOrderChangeTypeCreate CamelOilPrefetchOrderChangeType = "create" // 创建预拉取订单
CamelOilPrefetchOrderChangeTypeFetch CamelOilPrefetchOrderChangeType = "fetch" // 从骆驼平台拉取
CamelOilPrefetchOrderChangeTypeMatch CamelOilPrefetchOrderChangeType = "match" // 与用户订单匹配
CamelOilPrefetchOrderChangeTypeExpire CamelOilPrefetchOrderChangeType = "expire" // 过期失效
CamelOilPrefetchOrderChangeTypeInvalidate CamelOilPrefetchOrderChangeType = "invalidate" // 标记为失效
)
// CamelOilPrefetchOrderChangeTypeText 预拉取订单变更类型文本映射
var CamelOilPrefetchOrderChangeTypeText = map[CamelOilPrefetchOrderChangeType]string{
CamelOilPrefetchOrderChangeTypeCreate: "创建预拉取订单",
CamelOilPrefetchOrderChangeTypeFetch: "从平台拉取",
CamelOilPrefetchOrderChangeTypeMatch: "与订单匹配",
CamelOilPrefetchOrderChangeTypeExpire: "订单过期",
CamelOilPrefetchOrderChangeTypeInvalidate: "标记失效",
}
// ====================================================================================
// 预拉取订单业务配置常量
// ====================================================================================
const (
// CamelOilPrefetchOrderMinCapacity 预拉取订单最小库存阈值(当库存低于此值时触发补充)
CamelOilPrefetchOrderMinCapacity = 1
// CamelOilPrefetchOrderTargetCapacity 预拉取订单目标库存(补充时的目标数量)
CamelOilPrefetchOrderTargetCapacity = 20
// CamelOilPrefetchOrderExpireDuration 预拉取订单过期时间(小时)
CamelOilPrefetchOrderExpireDuration = 24
// CamelOilPrefetchMaxConcurrency 预拉取最大并发账号数量
CamelOilPrefetchMaxConcurrency = 5
// CamelOilPrefetchOrderLockKey Redis中预拉取订单的分布式锁键名前缀
CamelOilPrefetchOrderLockKey = "camel_oil_api:prefetch:order:lock:"
// CamelOilPrefetchTaskLockKey Redis中预拉取任务的分布式锁键名
CamelOilPrefetchTaskLockKey = "camel_oil_api:task:prefetch:lock"
)
// ====================================================================================
// 业务配置常量
// ====================================================================================
@@ -152,7 +220,7 @@ const (
CamelOilMinAvailableCapacity = 50
// CamelOilTargetOnlineAccounts 目标在线账号数量
CamelOilTargetOnlineAccounts = 5
CamelOilTargetOnlineAccounts = 1
// CamelOilOrderExpireDuration 订单支付超时时间(小时)
CamelOilOrderExpireDuration = 24

View File

@@ -22,7 +22,6 @@ type V1CamelOilAccountHistoryDao struct {
// V1CamelOilAccountHistoryColumns defines and stores column names for the table camel_oil_account_history.
type V1CamelOilAccountHistoryColumns struct {
Id string // 主键ID
HistoryUuid string // 历史记录唯一标识
AccountId string // 账号ID
ChangeType string // 变更类型create/login/offline/login_fail/pause/resume/invalidate/order_bind/order_complete/update/delete
StatusBefore string // 变更前状态
@@ -37,7 +36,6 @@ type V1CamelOilAccountHistoryColumns struct {
// v1CamelOilAccountHistoryColumns holds the columns for the table camel_oil_account_history.
var v1CamelOilAccountHistoryColumns = V1CamelOilAccountHistoryColumns{
Id: "id",
HistoryUuid: "history_uuid",
AccountId: "account_id",
ChangeType: "change_type",
StatusBefore: "status_before",

View File

@@ -29,6 +29,8 @@ type V1CamelOilOrderColumns struct {
PlatformOrderNo string // 骆驼平台订单号
Amount string // 订单金额
AlipayUrl string // 支付宝支付链接
CardPassword string // 卡密
CardNumber string // 卡号
Status string // 状态1待支付 2已支付 3支付超时 4下单失败
PayStatus string // 支付状态0未支付 1已支付 2超时
NotifyStatus string // 回调状态0未回调 1已回调 2回调失败
@@ -52,6 +54,8 @@ var v1CamelOilOrderColumns = V1CamelOilOrderColumns{
PlatformOrderNo: "platform_order_no",
Amount: "amount",
AlipayUrl: "alipay_url",
CardPassword: "card_password",
CardNumber: "card_number",
Status: "status",
PayStatus: "pay_status",
NotifyStatus: "notify_status",

View File

@@ -22,7 +22,6 @@ type V1CamelOilOrderHistoryDao struct {
// V1CamelOilOrderHistoryColumns defines and stores column names for the table camel_oil_order_history.
type V1CamelOilOrderHistoryColumns struct {
Id string // 主键ID
HistoryUuid string // 历史记录唯一标识
OrderNo string // 订单号
ChangeType string // 变更类型create/submit/get_pay_url/check_pay/paid/timeout/fail/callback_success/callback_fail
AccountId string // 关联账号ID
@@ -37,7 +36,6 @@ type V1CamelOilOrderHistoryColumns struct {
// v1CamelOilOrderHistoryColumns holds the columns for the table camel_oil_order_history.
var v1CamelOilOrderHistoryColumns = V1CamelOilOrderHistoryColumns{
Id: "id",
HistoryUuid: "history_uuid",
OrderNo: "order_no",
ChangeType: "change_type",
AccountId: "account_id",

View File

@@ -0,0 +1,107 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// V1CamelOilPrefetchOrderDao is the data access object for the table camel_oil_prefetch_order.
type V1CamelOilPrefetchOrderDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns V1CamelOilPrefetchOrderColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// V1CamelOilPrefetchOrderColumns defines and stores column names for the table camel_oil_prefetch_order.
type V1CamelOilPrefetchOrderColumns struct {
Id string // 主键ID
AccountId string // 拉取时使用的账号ID
AccountName string // 账号名称
Amount string // 预拉取订单金额
PlatformOrderNo string // 骆驼平台订单号
AlipayUrl string // 支付宝支付链接
Status string // 预拉取订单状态1待匹配 2已匹配 3已过期 4已失效
OrderId string // 匹配后的订单ID关联camel_oil_order表
MatchedAt string // 匹配时间
ExpireAt string // 预拉取订单过期时间通常为24小时后
FailureReason string // 失败原因
Remark string // 备注信息
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
DeletedAt string // 删除时间(软删除)
}
// v1CamelOilPrefetchOrderColumns holds the columns for the table camel_oil_prefetch_order.
var v1CamelOilPrefetchOrderColumns = V1CamelOilPrefetchOrderColumns{
Id: "id",
AccountId: "account_id",
AccountName: "account_name",
Amount: "amount",
PlatformOrderNo: "platform_order_no",
AlipayUrl: "alipay_url",
Status: "status",
OrderId: "order_id",
MatchedAt: "matched_at",
ExpireAt: "expire_at",
FailureReason: "failure_reason",
Remark: "remark",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
}
// NewV1CamelOilPrefetchOrderDao creates and returns a new DAO object for table data access.
func NewV1CamelOilPrefetchOrderDao(handlers ...gdb.ModelHandler) *V1CamelOilPrefetchOrderDao {
return &V1CamelOilPrefetchOrderDao{
group: "default",
table: "camel_oil_prefetch_order",
columns: v1CamelOilPrefetchOrderColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *V1CamelOilPrefetchOrderDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *V1CamelOilPrefetchOrderDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *V1CamelOilPrefetchOrderDao) Columns() V1CamelOilPrefetchOrderColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *V1CamelOilPrefetchOrderDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *V1CamelOilPrefetchOrderDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *V1CamelOilPrefetchOrderDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}

View File

@@ -0,0 +1,97 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// V1CamelOilPrefetchOrderHistoryDao is the data access object for the table camel_oil_prefetch_order_history.
type V1CamelOilPrefetchOrderHistoryDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns V1CamelOilPrefetchOrderHistoryColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// V1CamelOilPrefetchOrderHistoryColumns defines and stores column names for the table camel_oil_prefetch_order_history.
type V1CamelOilPrefetchOrderHistoryColumns struct {
Id string // 主键ID
PrefetchId string // 预拉取订单ID
ChangeType string // 变更类型create/fetch/match/expire/invalidate
AccountId string // 关联账号ID
AccountName string // 账号名称
RawData string // 原始响应数据
Remark string // 备注
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
DeletedAt string // 删除时间(软删除)
}
// v1CamelOilPrefetchOrderHistoryColumns holds the columns for the table camel_oil_prefetch_order_history.
var v1CamelOilPrefetchOrderHistoryColumns = V1CamelOilPrefetchOrderHistoryColumns{
Id: "id",
PrefetchId: "prefetch_id",
ChangeType: "change_type",
AccountId: "account_id",
AccountName: "account_name",
RawData: "raw_data",
Remark: "remark",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
}
// NewV1CamelOilPrefetchOrderHistoryDao creates and returns a new DAO object for table data access.
func NewV1CamelOilPrefetchOrderHistoryDao(handlers ...gdb.ModelHandler) *V1CamelOilPrefetchOrderHistoryDao {
return &V1CamelOilPrefetchOrderHistoryDao{
group: "default",
table: "camel_oil_prefetch_order_history",
columns: v1CamelOilPrefetchOrderHistoryColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *V1CamelOilPrefetchOrderHistoryDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *V1CamelOilPrefetchOrderHistoryDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *V1CamelOilPrefetchOrderHistoryDao) Columns() V1CamelOilPrefetchOrderHistoryColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *V1CamelOilPrefetchOrderHistoryDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *V1CamelOilPrefetchOrderHistoryDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *V1CamelOilPrefetchOrderHistoryDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}

View File

@@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"kami/internal/dao/internal"
)
// v1CamelOilPrefetchOrderDao is the data access object for the table camel_oil_prefetch_order.
// You can define custom methods on it to extend its functionality as needed.
type v1CamelOilPrefetchOrderDao struct {
*internal.V1CamelOilPrefetchOrderDao
}
var (
// V1CamelOilPrefetchOrder is a globally accessible object for table camel_oil_prefetch_order operations.
V1CamelOilPrefetchOrder = v1CamelOilPrefetchOrderDao{internal.NewV1CamelOilPrefetchOrderDao()}
)
// Add your custom methods and functionality below.

View File

@@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"kami/internal/dao/internal"
)
// v1CamelOilPrefetchOrderHistoryDao is the data access object for the table camel_oil_prefetch_order_history.
// You can define custom methods on it to extend its functionality as needed.
type v1CamelOilPrefetchOrderHistoryDao struct {
*internal.V1CamelOilPrefetchOrderHistoryDao
}
var (
// V1CamelOilPrefetchOrderHistory is a globally accessible object for table camel_oil_prefetch_order_history operations.
V1CamelOilPrefetchOrderHistory = v1CamelOilPrefetchOrderHistoryDao{internal.NewV1CamelOilPrefetchOrderHistoryDao()}
)
// Add your custom methods and functionality below.

View File

@@ -0,0 +1,109 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// V1CamelOilAccountDao is the data access object for the table camel_oil_account.
type V1CamelOilAccountDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns V1CamelOilAccountColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// V1CamelOilAccountColumns defines and stores column names for the table camel_oil_account.
type V1CamelOilAccountColumns struct {
Id string // 主键ID
AccountName string // 账号名称(备注)
Phone string // 手机号(登录后记录,不可重复)
Token string // 登录Token
Status string // 状态1待登录 2在线 3暂停 4已失效 5登录失败
TokenExpireAt string // Token过期时间
LastLoginAt string // 最后登录时间
LastUsedAt string // 最后使用时间
DailyOrderCount string // 当日已下单数量
DailyOrderDate string // 当日订单日期
TotalOrderCount string // 累计下单数量
FailureReason string // 失败原因
Remark string // 备注信息
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
DeletedAt string // 删除时间(软删除)
}
// v1CamelOilAccountColumns holds the columns for the table camel_oil_account.
var v1CamelOilAccountColumns = V1CamelOilAccountColumns{
Id: "id",
AccountName: "account_name",
Phone: "phone",
Token: "token",
Status: "status",
TokenExpireAt: "token_expire_at",
LastLoginAt: "last_login_at",
LastUsedAt: "last_used_at",
DailyOrderCount: "daily_order_count",
DailyOrderDate: "daily_order_date",
TotalOrderCount: "total_order_count",
FailureReason: "failure_reason",
Remark: "remark",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
}
// NewV1CamelOilAccountDao creates and returns a new DAO object for table data access.
func NewV1CamelOilAccountDao(handlers ...gdb.ModelHandler) *V1CamelOilAccountDao {
return &V1CamelOilAccountDao{
group: "default",
table: "camel_oil_account",
columns: v1CamelOilAccountColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *V1CamelOilAccountDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *V1CamelOilAccountDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *V1CamelOilAccountDao) Columns() V1CamelOilAccountColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *V1CamelOilAccountDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *V1CamelOilAccountDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *V1CamelOilAccountDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}

View File

@@ -0,0 +1,99 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// V1CamelOilAccountHistoryDao is the data access object for the table camel_oil_account_history.
type V1CamelOilAccountHistoryDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns V1CamelOilAccountHistoryColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// V1CamelOilAccountHistoryColumns defines and stores column names for the table camel_oil_account_history.
type V1CamelOilAccountHistoryColumns struct {
Id string // 主键ID
HistoryUuid string // 历史记录唯一标识
AccountId string // 账号ID
ChangeType string // 变更类型create/login/offline/login_fail/pause/resume/invalidate/order_bind/order_complete/update/delete
StatusBefore string // 变更前状态
StatusAfter string // 变更后状态
FailureCount string // 失败次数
Remark string // 备注
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
DeletedAt string // 删除时间(软删除)
}
// v1CamelOilAccountHistoryColumns holds the columns for the table camel_oil_account_history.
var v1CamelOilAccountHistoryColumns = V1CamelOilAccountHistoryColumns{
Id: "id",
HistoryUuid: "history_uuid",
AccountId: "account_id",
ChangeType: "change_type",
StatusBefore: "status_before",
StatusAfter: "status_after",
FailureCount: "failure_count",
Remark: "remark",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
}
// NewV1CamelOilAccountHistoryDao creates and returns a new DAO object for table data access.
func NewV1CamelOilAccountHistoryDao(handlers ...gdb.ModelHandler) *V1CamelOilAccountHistoryDao {
return &V1CamelOilAccountHistoryDao{
group: "default",
table: "camel_oil_account_history",
columns: v1CamelOilAccountHistoryColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *V1CamelOilAccountHistoryDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *V1CamelOilAccountHistoryDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *V1CamelOilAccountHistoryDao) Columns() V1CamelOilAccountHistoryColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *V1CamelOilAccountHistoryDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *V1CamelOilAccountHistoryDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *V1CamelOilAccountHistoryDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}

View File

@@ -0,0 +1,119 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// V1CamelOilOrderDao is the data access object for the table camel_oil_order.
type V1CamelOilOrderDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns V1CamelOilOrderColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// V1CamelOilOrderColumns defines and stores column names for the table camel_oil_order.
type V1CamelOilOrderColumns struct {
Id string // 主键ID
OrderNo string // 系统订单号
MerchantOrderId string // 商户订单号
AccountId string // 使用的账号ID
AccountName string // 账号名称
PlatformOrderNo string // 骆驼平台订单号
Amount string // 订单金额
AlipayUrl string // 支付宝支付链接
CardPassword string // 卡密
CardNumber string // 卡号
Status string // 状态1待支付 2已支付 3支付超时 4下单失败
PayStatus string // 支付状态0未支付 1已支付 2超时
NotifyStatus string // 回调状态0未回调 1已回调 2回调失败
NotifyCount string // 回调次数
LastCheckAt string // 最后检测支付时间
PaidAt string // 支付完成时间
Attach string // 附加信息
FailureReason string // 失败原因
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
DeletedAt string // 删除时间(软删除)
}
// v1CamelOilOrderColumns holds the columns for the table camel_oil_order.
var v1CamelOilOrderColumns = V1CamelOilOrderColumns{
Id: "id",
OrderNo: "order_no",
MerchantOrderId: "merchant_order_id",
AccountId: "account_id",
AccountName: "account_name",
PlatformOrderNo: "platform_order_no",
Amount: "amount",
AlipayUrl: "alipay_url",
CardPassword: "card_password",
CardNumber: "card_number",
Status: "status",
PayStatus: "pay_status",
NotifyStatus: "notify_status",
NotifyCount: "notify_count",
LastCheckAt: "last_check_at",
PaidAt: "paid_at",
Attach: "attach",
FailureReason: "failure_reason",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
}
// NewV1CamelOilOrderDao creates and returns a new DAO object for table data access.
func NewV1CamelOilOrderDao(handlers ...gdb.ModelHandler) *V1CamelOilOrderDao {
return &V1CamelOilOrderDao{
group: "default",
table: "camel_oil_order",
columns: v1CamelOilOrderColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *V1CamelOilOrderDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *V1CamelOilOrderDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *V1CamelOilOrderDao) Columns() V1CamelOilOrderColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *V1CamelOilOrderDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *V1CamelOilOrderDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *V1CamelOilOrderDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}

View File

@@ -0,0 +1,99 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// V1CamelOilOrderHistoryDao is the data access object for the table camel_oil_order_history.
type V1CamelOilOrderHistoryDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns V1CamelOilOrderHistoryColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// V1CamelOilOrderHistoryColumns defines and stores column names for the table camel_oil_order_history.
type V1CamelOilOrderHistoryColumns struct {
Id string // 主键ID
HistoryUuid string // 历史记录唯一标识
OrderNo string // 订单号
ChangeType string // 变更类型create/submit/get_pay_url/check_pay/paid/timeout/fail/callback_success/callback_fail
AccountId string // 关联账号ID
AccountName string // 账号名称
RawData string // 原始响应数据
Remark string // 备注
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
DeletedAt string // 删除时间(软删除)
}
// v1CamelOilOrderHistoryColumns holds the columns for the table camel_oil_order_history.
var v1CamelOilOrderHistoryColumns = V1CamelOilOrderHistoryColumns{
Id: "id",
HistoryUuid: "history_uuid",
OrderNo: "order_no",
ChangeType: "change_type",
AccountId: "account_id",
AccountName: "account_name",
RawData: "raw_data",
Remark: "remark",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
}
// NewV1CamelOilOrderHistoryDao creates and returns a new DAO object for table data access.
func NewV1CamelOilOrderHistoryDao(handlers ...gdb.ModelHandler) *V1CamelOilOrderHistoryDao {
return &V1CamelOilOrderHistoryDao{
group: "default",
table: "camel_oil_order_history",
columns: v1CamelOilOrderHistoryColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *V1CamelOilOrderHistoryDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *V1CamelOilOrderHistoryDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *V1CamelOilOrderHistoryDao) Columns() V1CamelOilOrderHistoryColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *V1CamelOilOrderHistoryDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *V1CamelOilOrderHistoryDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *V1CamelOilOrderHistoryDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}

View File

@@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package model
import (
"kami/internal/internal/model/internal"
)
// v1CamelOilAccountDao is the data access object for the table camel_oil_account.
// You can define custom methods on it to extend its functionality as needed.
type v1CamelOilAccountDao struct {
*internal.V1CamelOilAccountDao
}
var (
// V1CamelOilAccount is a globally accessible object for table camel_oil_account operations.
V1CamelOilAccount = v1CamelOilAccountDao{internal.NewV1CamelOilAccountDao()}
)
// Add your custom methods and functionality below.

View File

@@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package model
import (
"kami/internal/internal/model/internal"
)
// v1CamelOilAccountHistoryDao is the data access object for the table camel_oil_account_history.
// You can define custom methods on it to extend its functionality as needed.
type v1CamelOilAccountHistoryDao struct {
*internal.V1CamelOilAccountHistoryDao
}
var (
// V1CamelOilAccountHistory is a globally accessible object for table camel_oil_account_history operations.
V1CamelOilAccountHistory = v1CamelOilAccountHistoryDao{internal.NewV1CamelOilAccountHistoryDao()}
)
// Add your custom methods and functionality below.

View File

@@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package model
import (
"kami/internal/internal/model/internal"
)
// v1CamelOilOrderDao is the data access object for the table camel_oil_order.
// You can define custom methods on it to extend its functionality as needed.
type v1CamelOilOrderDao struct {
*internal.V1CamelOilOrderDao
}
var (
// V1CamelOilOrder is a globally accessible object for table camel_oil_order operations.
V1CamelOilOrder = v1CamelOilOrderDao{internal.NewV1CamelOilOrderDao()}
)
// Add your custom methods and functionality below.

View File

@@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package model
import (
"kami/internal/internal/model/internal"
)
// v1CamelOilOrderHistoryDao is the data access object for the table camel_oil_order_history.
// You can define custom methods on it to extend its functionality as needed.
type v1CamelOilOrderHistoryDao struct {
*internal.V1CamelOilOrderHistoryDao
}
var (
// V1CamelOilOrderHistory is a globally accessible object for table camel_oil_order_history operations.
V1CamelOilOrderHistory = v1CamelOilOrderHistoryDao{internal.NewV1CamelOilOrderHistoryDao()}
)
// Add your custom methods and functionality below.

View File

@@ -36,37 +36,6 @@ func (s *sCamelOil) GetAvailableOrderCapacity(ctx context.Context) (capacity int
return result.TotalCapacity, nil
}
// CheckAndTriggerAccountLogin 检查容量并触发账号登录
// 如果可用订单容量<50,触发账号登录任务
func (s *sCamelOil) CheckAndTriggerAccountLogin(ctx context.Context) (err error) {
// 1. 获取当前可用容量
capacity, err := s.GetAvailableOrderCapacity(ctx)
if err != nil {
glog.Error(ctx, "获取可用订单容量失败", err)
return err
}
glog.Info(ctx, "当前可用订单容量", capacity)
// 2. 如果容量充足,无需登录
if capacity >= 50 {
return nil
}
// 3. 计算需要登录的账号数量
needCount := (50 - capacity + 9) / 10 // 向上取整
glog.Info(ctx, "可用订单容量不足,需要登录账号", needCount)
// 4. 触发账号登录任务
// 这里会在cron任务中调用LoginAccount方法
// 暂时只记录日志,具体登录逻辑在account_login.go中实现
if _, err = s.BatchLoginAccounts(ctx, needCount); err != nil {
glog.Error(ctx, "批量登录失败", err)
}
return nil
}
// GetAccountPoolStatus 获取账号池状态统计
func (s *sCamelOil) GetAccountPoolStatus(ctx context.Context) (status map[string]interface{}, err error) {
m := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1())

View File

@@ -43,7 +43,6 @@ func (s *sCamelOil) GetAccountHistory(ctx context.Context, req *v1.AccountHistor
items := make([]v1.AccountHistoryItem, 0, len(histories))
for _, history := range histories {
items = append(items, v1.AccountHistoryItem{
HistoryUuid: history.HistoryUuid,
AccountId: history.AccountId,
ChangeType: consts.CamelOilAccountChangeType(history.ChangeType),
ChangeText: getAccountChangeTypeText(history.ChangeType),

View File

@@ -3,77 +3,91 @@ package camel_oil
import (
"context"
"fmt"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/os/gtime"
"kami/internal/consts"
"kami/internal/dao"
"kami/internal/model/do"
"kami/utility/config"
"kami/utility/integration/camel_oil_api"
"kami/utility/integration/pig"
"sync"
"sync/atomic"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/glog"
)
// LoginAccount 执行账号登录流程
// 注意:当前使用假数据,实际应对接骆驼加油平台和接码平台
func (s *sCamelOil) LoginAccount(ctx context.Context) (err error) {
// 对接接码平台
phoneNumber, err := pig.NewClient().GetAccountInfo(ctx)
if err != nil {
return gerror.Wrap(err, "获取手机号失败")
// 对接接码平台,获取手机号并检查是否已存在
var phoneNumber string
for {
phoneNumber, err = pig.NewClient().GetAccountInfo(ctx)
if err != nil {
return gerror.Wrap(err, "获取手机号失败")
}
// 检查手机号是否已存在
existingAccount, checkErr := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilAccount.Columns().Phone, phoneNumber).
One()
if checkErr != nil {
return gerror.Wrap(checkErr, "检查手机号是否存在失败")
}
// 如果手机号已存在,继续获取新的手机号
if existingAccount == nil {
// 手机号不存在,可以使用
break
}
glog.Infof(ctx, "手机号已存在,重新获取: %s", phoneNumber)
}
isOk, err := camel_oil_api.NewClient().SendCaptcha(ctx, phoneNumber)
if err != nil {
return gerror.Wrap(err, "发送验证码失败")
}
if !isOk {
return gerror.New("获取验证码失败")
}
accountId, err := s.CreateAccount(ctx, phoneNumber, "创建账号")
if err != nil {
return gerror.Wrap(err, "创建账号失败")
}
//发送验证码
isOk, err := camel_oil_api.NewClient().SendCaptcha(ctx, phoneNumber)
if err != nil {
_ = s.UpdateAccountStatus(ctx, accountId, consts.CamelOilAccountStatusInvalid, consts.CamelOilAccountChangeTypeLoginFail, "获取验证码失败")
return gerror.Wrap(err, "发送验证码失败")
}
if !isOk {
_ = s.UpdateAccountStatus(ctx, accountId, consts.CamelOilAccountStatusInvalid, consts.CamelOilAccountChangeTypeLoginFail, "获取验证码失败")
return gerror.New("获取验证码失败")
}
// 更新状态为登录中
_, err = dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilAccount.Columns().Id, accountId).
Update(do.V1CamelOilAccount{
Status: consts.CamelOilAccountStatusSendCode,
UpdatedAt: gtime.Now(),
})
if err != nil {
_ = s.UpdateAccountStatus(ctx, accountId, consts.CamelOilAccountStatusInvalid, consts.CamelOilAccountChangeTypeLoginFail, "获取验证码失败")
return gerror.Wrap(err, "更新账号状态为登录中失败")
}
_ = s.UpdateAccountStatus(ctx, accountId, consts.CamelOilAccountStatusSendCode, consts.CamelOilAccountChangeTypeLogin, "获取验证码失败")
_ = s.UpdateAccountStatus(ctx, accountId, consts.CamelOilAccountStatusSendCode, consts.CamelOilAccountChangeTypeLogin, "获取验证码成功")
return nil
}
// BatchLoginAccounts 批量登录账号
func (s *sCamelOil) BatchLoginAccounts(ctx context.Context, count int) (successCount int, err error) {
func (s *sCamelOil) BatchLoginAccounts(ctx context.Context, count int64) (successCount int64, err error) {
if count <= 0 {
return 0, gerror.New("登录数量必须大于0")
}
// 逐个登录账号
successCount = 0
cycleCount := 0
for {
cycleCount++
if successCount >= count || cycleCount >= 100 {
for range 10 {
if successCount >= count {
break
}
loginErr := s.LoginAccount(ctx)
if loginErr != nil {
glog.Errorf(ctx, "账号登录失败ID: %d, 错误: %v", loginErr)
continue
wg := sync.WaitGroup{}
sem := make(chan struct{}, 1)
for i := 0; i < int(count-successCount); i++ {
sem <- struct{}{}
wg.Go(func() {
defer func() { <-sem }()
loginErr := s.LoginAccount(ctx)
if loginErr != nil {
glog.Errorf(ctx, "账号登录失败ID: %d, 错误: %v", loginErr)
return
}
atomic.AddInt64(&successCount, 1)
})
}
successCount += 1
wg.Wait()
}
glog.Infof(ctx, "批量登录完成,成功: %d", successCount)
return successCount, nil
}
@@ -85,7 +99,6 @@ func (s *sCamelOil) markAccountInvalid(ctx context.Context, accountId int64, rea
Update(do.V1CamelOilAccount{
Status: consts.CamelOilAccountStatusInvalid,
FailureReason: reason,
UpdatedAt: gtime.Now(),
})
if err != nil {
return gerror.Wrap(err, "标记账号为已失效失败")
@@ -99,38 +112,3 @@ func (s *sCamelOil) markAccountInvalid(ctx context.Context, accountId int64, rea
glog.Warningf(ctx, "账号已标记为失效ID: %d, 原因: %s", accountId, reason)
return nil
}
// CheckAndLoginAccounts 检查容量并登录账号
// 根据当前可用订单容量,决定是否需要登录新账号
func (s *sCamelOil) CheckAndLoginAccounts(ctx context.Context) (err error) {
// 计算当前可用订单容量
capacity, err := s.GetAvailableOrderCapacity(ctx)
if err != nil {
return gerror.Wrap(err, "计算可用容量失败")
}
glog.Infof(ctx, "当前可用订单容量: %d", capacity)
// 容量充足,无需登录
if capacity >= 50 {
glog.Infof(ctx, "当前容量充足 (%d >= 50),无需登录新账号", capacity)
return nil
}
// 计算需要登录的账号数量
needCapacity := 50 - capacity
needAccounts := (needCapacity + 9) / 10 // 向上取整
glog.Infof(ctx, "当前容量不足 (%d < 50),需要登录 %d 个账号", capacity, needAccounts)
// 批量登录账号
successCount, err := s.BatchLoginAccounts(ctx, needAccounts)
if err != nil {
return gerror.Wrap(err, "批量登录账号失败")
}
glog.Infof(ctx, "登录账号完成,成功: %d 个", successCount)
return nil
}
// 注意RecordAccountHistory 方法已在 account.go 中定义,此处不重复定义

View File

@@ -3,6 +3,7 @@ package camel_oil
import (
"context"
"fmt"
"time"
"kami/internal/consts"
"kami/internal/dao"
@@ -16,38 +17,79 @@ import (
"github.com/gogf/gf/v2/os/gtime"
)
// CronAccountLoginTask 账号登录任务 - 由cron调度器定期调用
func (s *sCamelOil) CronAccountLoginTask(ctx context.Context) error {
// CronAccountPrefetchTask 账户预拉取定时任务 - 由cron调度器定期调用
// 流程:并发拉取账户到指定数量
func (s *sCamelOil) CronAccountPrefetchTask(ctx context.Context) error {
glog.Info(ctx, "开始执行账户预拉取任务")
// 检查可用订单容
capacity, err := s.GetAvailableOrderCapacity(ctx)
// 1. 获取当前在线账号数
m := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1())
onlineCount, err := m.Where(dao.V1CamelOilAccount.Columns().Status, consts.CamelOilAccountStatusOnline).
WhereOr(dao.V1CamelOilAccount.Columns().Status, consts.CamelOilAccountStatusSendCode).
Count()
if err != nil {
glog.Error(ctx, "获取可用容量失败:", err)
return err
glog.Errorf(ctx, "获取在线账号数量失败: %v", err)
onlineCount = 0
}
glog.Infof(ctx, "当前可用订单容量: %d", capacity)
glog.Infof(ctx, "当前在线账号数量: %d, 目标数量: %d", onlineCount, consts.CamelOilTargetOnlineAccounts)
// 如果容量低于50继续登录新账号
if capacity <= 50 {
err = s.CheckAndTriggerAccountLogin(ctx)
// 2. 如果在线账号少于目标数,触发并发登录
if onlineCount < consts.CamelOilTargetOnlineAccounts {
needCount := consts.CamelOilTargetOnlineAccounts - onlineCount
glog.Infof(ctx, "在线账号不足,需要登录 %d 个账号", needCount)
// 使用并发登录提高效率
successCount, err := s.BatchLoginAccounts(ctx, int64(needCount))
if err != nil {
glog.Error(ctx, "触发账号登录失败:", err)
return err
glog.Errorf(ctx, "批量登录账号失败: %v", err)
// 不返回错误,继续执行
} else {
glog.Infof(ctx, "成功登录 %d 个账号", successCount)
}
}
return nil
}
// CronPrefetchOrderSupplementTask 预拉取订单补充定时任务 - 由cron调度器定期调用
// 流程:检查预拉取订单库存,不足时补充
func (s *sCamelOil) CronPrefetchOrderSupplementTask(ctx context.Context) error {
glog.Info(ctx, "开始执行预拉取订单补充任务")
// 1. 检查预拉取订单库存
capacity, err := s.GetPrefetchOrderCapacity(ctx)
if err != nil {
glog.Errorf(ctx, "获取预拉取订单库存失败: %v", err)
return err
}
glog.Infof(ctx, "当前预拉取订单库存: %d", capacity)
// 2. 如果库存不足则补充
if capacity < consts.CamelOilPrefetchOrderMinCapacity {
glog.Infof(ctx, "预拉取订单库存不足,开始补充任务")
supplementedCount, err := s.SupplementPrefetchOrders(ctx)
if err != nil {
glog.Errorf(ctx, "预拉取订单补充失败: %v", err)
return err
}
glog.Infof(ctx, "预拉取订单补充完成,补充数量: %d", supplementedCount)
} else {
glog.Infof(ctx, "预拉取订单库存充足 (%d >= %d),无需补充", capacity, consts.CamelOilPrefetchOrderMinCapacity)
}
glog.Info(ctx, "预拉取订单补充任务完成")
return nil
}
// CronOrderPaymentCheckTask 订单支付状态检测任务 - 由cron调度器定期调用
func (s *sCamelOil) CronOrderPaymentCheckTask(ctx context.Context) error {
glog.Info(ctx, "开始执行订单支付状态检测任务")
// 查询待支付订单创建时间在24小时内
var orders []*entity.V1CamelOilOrder
err := dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilOrder.Columns().PayStatus, consts.CamelOilPaymentStatusUnpaid).
WhereGTE(dao.V1CamelOilOrder.Columns().CreatedAt, gtime.Now().AddDate(0, 0, -1)).
WhereGTE(dao.V1CamelOilOrder.Columns().CreatedAt, gtime.Now().Add(time.Hour*6)).
Scan(&orders)
if err != nil {
@@ -62,7 +104,6 @@ func (s *sCamelOil) CronOrderPaymentCheckTask(ctx context.Context) error {
glog.Infof(ctx, "查询到 %d 个待支付订单", len(orders))
// 检测每个订单的支付状态(使用假数据)
paidCount := 0
timeoutCount := 0
@@ -70,32 +111,49 @@ func (s *sCamelOil) CronOrderPaymentCheckTask(ctx context.Context) error {
accountInfo, err2 := s.GetAccountInfo(ctx, order.AccountId)
if err2 != nil {
glog.Error(ctx, "获取账号信息失败:", err2)
return err2
// 记录该订单检查失败
_ = s.UpdateOrderStatus(ctx, order.Id, consts.CamelOilOrderStatusFailed, consts.CamelOilOrderChangeTypeFail, "", fmt.Sprintf("查询账户失败: %v", err2))
continue
}
ok, err := camel_oil_api.NewClient().QueryOrder(ctx, accountInfo.Phone, accountInfo.Token, order.PlatformOrderNo)
if err != nil {
glog.Error(ctx, "查询订单状态失败:", err)
// 查询订单状态
queryResult, err2 := camel_oil_api.NewClient().QueryOrder(ctx, accountInfo.Phone, accountInfo.Token, order.PlatformOrderNo)
if err2 != nil {
glog.Error(ctx, "查询订单状态失败:", err2)
_ = s.RecordOrderHistory(ctx, order.OrderNo, consts.CamelOilOrderChangeType("query_failed"), "", fmt.Sprintf("查询订单失败: %v", err))
continue
}
if ok {
// 更新状态
// 订单已支付
if queryResult != nil {
_ = s.fillOrderCard(ctx, order.OrderNo, queryResult.CardNumber, queryResult.CardNumber)
if order.PayStatus != int(consts.CamelOilPaymentStatusPaid) {
glog.Infof(ctx, "订单%s已支付金额: %.2f", order.OrderNo, queryResult.Balance)
_, _ = dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilOrder.Columns().Id, order.Id).
Update(&do.V1CamelOilOrder{
CardNumber: queryResult.CardNumber,
CardPassword: queryResult.CardPassword,
PaidAt: gtime.Now(),
PayStatus: consts.CamelOilPaymentStatusPaid,
})
_ = s.RecordOrderHistory(ctx, order.OrderNo, consts.CamelOilOrderChangeTypePaid, "", fmt.Sprintf("支付成功,金额: %.2f", queryResult.Balance))
paidCount++
}
continue
}
// 订单未支付检查是否超时24小时
if gtime.Now().Sub(order.CreatedAt).Hours() >= 1 {
glog.Warningf(ctx, "订单%s支付超时创建时间: %v", order.OrderNo, order.CreatedAt)
_, _ = dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilOrder.Columns().Id, order.Id).
Update(&do.V1CamelOilOrder{
PaidAt: gtime.Now(),
PayStatus: int(consts.CamelOilPaymentStatusPaid),
Status: consts.CamelOilOrderStatusFailed,
PayStatus: consts.CamelOilPaymentStatusTimeout,
FailureReason: "支付时间超过24小时支付超时",
})
}
// 模拟支付状态检测
// 如果订单创建超过1小时标记为超时
if gtime.Now().Sub(order.CreatedAt).Hours() > 1 {
// 更新为超时
_, _ = dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilOrder.Columns().Id, order.Id).
Update(&do.V1CamelOilOrder{
PayStatus: int(consts.CamelOilPaymentStatusTimeout),
FailureReason: "支付时间超过一个小时,支付超时",
})
_ = s.RecordOrderHistory(ctx, order.OrderNo, "payment_timeout", "", "订单支付超时")
_ = s.RecordOrderHistory(ctx, order.OrderNo, consts.CamelOilOrderChangeTypeTimeout, "", "订单支付超时")
timeoutCount++
}
}
@@ -187,6 +245,15 @@ func (s *sCamelOil) CronVerifyCodeCheckTask(ctx context.Context) error {
camelClient := camel_oil_api.NewClient()
for _, account := range accounts {
//如果时间超过 1 分钟,就是过期
if gtime.Now().Sub(account.CreatedAt).Minutes() > 1 {
glog.Warningf(ctx, "验证码已过期账号ID: %d, 手机号: %s", account.Id, account.Phone)
_ = s.UpdateAccountStatus(ctx, account.Id, consts.CamelOilAccountStatusInvalid,
consts.CamelOilAccountChangeTypeLoginFail, "验证码已过期")
failCount++
continue
}
// 从野猪平台检测验证码是否已接收
verifyCode, received, err := pigClient.CheckVerifyCode(ctx, account.Phone)
if err != nil {
@@ -233,6 +300,7 @@ func (s *sCamelOil) CronVerifyCodeCheckTask(ctx context.Context) error {
consts.CamelOilAccountChangeTypeLogin, fmt.Sprintf("登录成功,手机号: %s", account.Phone))
glog.Infof(ctx, "账号登录成功ID: %d, 手机号: %s, Token: %s", account.Id, account.Phone, token)
successCount++
}

View File

@@ -3,14 +3,14 @@ package camel_oil
import (
"context"
"fmt"
"github.com/gogf/gf/v2/database/gdb"
"kami/utility/utils"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gmlock"
"github.com/gogf/gf/v2/os/gtime"
"github.com/shopspring/decimal"
"kami/utility/integration/camel_oil_api"
"kami/utility/utils"
v1 "kami/api/camel_oil/v1"
"kami/internal/consts"
@@ -24,141 +24,152 @@ import (
// 订单管理相关方法
// ====================================================================================
// UpdateOrderStatus 更新订单状态并记录历史
func (s *sCamelOil) UpdateOrderStatus(ctx context.Context, orderId int64, newStatus consts.CamelOilOrderStatus, operationType consts.CamelOilOrderChangeType, rawData string, description string) (err error) {
m := dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1())
// 获取当前订单信息
var order *entity.V1CamelOilOrder
err = m.Where(dao.V1CamelOilOrder.Columns().Id, orderId).Scan(&order)
if err != nil {
return gerror.Wrap(err, "查询订单失败")
}
if order == nil {
return gerror.New("订单不存在")
}
oldStatus := consts.CamelOilOrderStatus(order.Status)
// 如果状态没有变化,则不更新
if oldStatus == newStatus {
return nil
}
// 更新订单状态
_, err = m.Where(dao.V1CamelOilOrder.Columns().Id, orderId).Update(&do.V1CamelOilOrder{
Status: int(newStatus),
})
if err != nil {
return gerror.Wrap(err, "更新订单状态失败")
}
// 记录订单变更历史
_ = s.RecordOrderHistory(ctx, order.OrderNo, operationType, rawData, description)
g.Log().Infof(ctx, "订单状态更新成功,订单号=%s, 原状态=%d, 新状态=%d", order.OrderNo, oldStatus, newStatus)
return nil
}
// SubmitOrder 提交订单并返回支付宝支付链接
func (s *sCamelOil) SubmitOrder(ctx context.Context, req *v1.SubmitOrderReq) (res *v1.SubmitOrderRes, err error) {
// 1. 检查可用订单容量低于50则触发账号登录任务
capacity, err := s.GetAvailableOrderCapacity(ctx)
if err != nil {
return nil, gerror.Wrap(err, "检查账号容量失败")
}
// 容量不足50触发异步登录任务
if capacity <= 50 {
g.Log().Infof(ctx, "可用订单容量不足50当前%d触发账号登录任务", capacity)
go func() {
if err = s.CheckAndTriggerAccountLogin(context.Background()); err != nil {
g.Log().Errorf(ctx, "触发账号登录任务失败:%v", err)
}
}()
}
accountCount, _ := s.GetOrderCountByStatus(ctx, consts.CamelOilAccountStatusOnline)
for i := 0; i < accountCount; i++ {
account, err := s.GetAvailableAccount(ctx)
if err != nil {
return nil, gerror.Wrap(err, "获取可用账号失败")
}
if account == nil {
return nil, gerror.New("暂无可用账号,请稍后重试")
}
platformOrderId, payId, err := camel_oil_api.NewClient().CreateOrder(ctx, account.Phone, account.Token, req.Amount)
if err != nil {
if err.Error() == "auth_error" {
_ = s.UpdateAccountStatus(ctx, account.Id, consts.CamelOilAccountStatusInvalid, consts.CamelOilAccountChangeTypeInvalidate, "账号token失效")
continue
}
return nil, err
}
// 生成系统订单号
orderNo := fmt.Sprintf("CO%s", utils.GenerateRandomUUID())
gmlock.LockFunc(fmt.Sprintf("camelSubmitOrder_%d", account.Id), func() {
// 4. 保存订单记录并更新账号使用信息(使用事务)
err = dao.V1CamelOilOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
// 插入订单
_, err = dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).Insert(&do.V1CamelOilOrder{
OrderNo: orderNo,
MerchantOrderId: req.MerchantOrderId,
AccountId: account.Id,
AccountName: account.AccountName,
PlatformOrderNo: platformOrderId,
Amount: decimal.NewFromFloat(req.Amount),
AlipayUrl: payId,
Status: 1, // 1=待支付
PayStatus: 0, // 0=未支付
NotifyStatus: 0, // 0=未回调
NotifyCount: 0,
Attach: req.Attach,
})
if err != nil {
return gerror.Wrap(err, "保存订单记录失败")
}
_, err = dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilAccount.Columns().Id, account.Id).
Increment(dao.V1CamelOilAccount.Columns().DailyOrderCount, 1)
if err != nil {
return gerror.Wrap(err, "更新账号使用记录失败")
}
_, err = dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilAccount.Columns().Id, account.Id).
Increment(dao.V1CamelOilAccount.Columns().TotalOrderCount, 1)
if err != nil {
return gerror.Wrap(err, "更新账号使用记录失败")
}
// 检查账号是否达到单日限额10单
var updatedAccount *entity.V1CamelOilAccount
err = dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilAccount.Columns().Id, account.Id).
Scan(&updatedAccount)
if err != nil {
return gerror.Wrap(err, "查询账号失败")
}
if updatedAccount.DailyOrderCount >= 10 {
// 达到限额,标记为已暂停 (status=3)
_, err = dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilAccount.Columns().Id, account.Id).
Update(do.V1CamelOilAccount{
Status: consts.CamelOilAccountStatusPaused, // 3=暂停
})
if err != nil {
return gerror.Wrap(err, "更新账号状态失败")
}
g.Log().Infof(ctx, "账号[%s]达到单日限额10单已暂停", account.Phone)
}
// 记录订单历史
_, err = dao.V1CamelOilOrderHistory.Ctx(ctx).DB(config.GetDatabaseV1()).Data(&do.V1CamelOilOrderHistory{
HistoryUuid: utils.GenerateRandomUUID(),
OrderNo: orderNo,
ChangeType: "create",
AccountId: account.Id,
AccountName: account.AccountName,
Remark: fmt.Sprintf("创建订单:商户订单号=%s平台订单号=%s", req.MerchantOrderId, platformOrderId),
}).Insert()
if err != nil {
g.Log().Errorf(ctx, "记录订单历史失败:%v", err)
}
// 记录账号历史
_ = s.RecordAccountHistory(ctx, account.Id, consts.CamelOilAccountChangeTypeOrderBind,
consts.CamelOilAccountStatus(account.Status), consts.CamelOilAccountStatus(account.Status),
fmt.Sprintf("绑定订单:%s", orderNo))
return nil
})
})
if err != nil {
return nil, err
}
// 5. 返回支付链接
res = &v1.SubmitOrderRes{
OrderNo: orderNo,
AlipayUrl: "",
var order *entity.V1CamelOilOrder
_ = dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1CamelOilOrder.Columns().MerchantOrderId, req.MerchantOrderId).Scan(&order)
if order != nil {
return &v1.SubmitOrderRes{
OrderNo: order.OrderNo,
AlipayUrl: order.AlipayUrl,
Amount: req.Amount,
CreatedAt: gtime.Now(),
}
g.Log().Infof(ctx, "订单创建成功:商户订单号=%s平台订单号=%s账号=%s 系统订单号=%s",
req.MerchantOrderId, platformOrderId, account.Phone, orderNo)
return res, nil
CreatedAt: order.CreatedAt,
}, nil
}
return nil, gerror.New("暂无可用账号,请稍后重试")
//// 直接拉取一单
//platformOrderId, payUrl, err := camel_oil_api.NewClient().CreateOrder(ctx, account.Phone, account.Token, req.Amount)
//if err != nil {
// if err.Error() == "auth_error" {
// _ = s.UpdateAccountStatus(ctx, account.Id, consts.CamelOilAccountStatusInvalid, consts.CamelOilAccountChangeTypeInvalidate, "账号token失效")
// }
// return nil, gerror.Wrap(err, "拉取订单失败")
//}
prefetchOrder, err := s.PrefetchOrderConcurrently(ctx, req.Amount)
if err != nil {
return nil, gerror.Wrap(err, "拉取订单失败")
}
// 2. 创建空订单记录
orderNo := fmt.Sprintf("CO%s", utils.GenerateRandomUUID())
_, err = dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).Insert(&do.V1CamelOilOrder{
OrderNo: orderNo,
MerchantOrderId: req.MerchantOrderId,
Amount: decimal.NewFromFloat(req.Amount),
Status: consts.CamelOilOrderStatusPending, // 1=待支付
PayStatus: consts.CamelOilPaymentStatusUnpaid, // 0=未支付
NotifyStatus: consts.CamelOilCallbackStatusPending, // 0=未回调
NotifyCount: 0,
AccountId: prefetchOrder.AccountId,
AccountName: prefetchOrder.AccountName,
PlatformOrderNo: prefetchOrder.PlatformOrderNo,
AlipayUrl: prefetchOrder.AlipayUrl,
Attach: req.Attach,
})
if err != nil {
return nil, gerror.Wrap(err, "更新订单失败")
}
// 更新账号使用记录
_, _ = dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilAccount.Columns().Id, prefetchOrder.AccountId).
Increment(dao.V1CamelOilAccount.Columns().DailyOrderCount, 1)
_, _ = dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilAccount.Columns().Id, prefetchOrder.AccountId).
Increment(dao.V1CamelOilAccount.Columns().TotalOrderCount, 1)
glog.Infof(ctx, "订单提交成功: 订单号=%s", orderNo)
res = &v1.SubmitOrderRes{
OrderNo: orderNo,
AlipayUrl: prefetchOrder.AlipayUrl,
Amount: req.Amount,
CreatedAt: gtime.Now(),
}
return res, nil
}
// ====================================================================================
// 卡密填写相关方法
// ====================================================================================
// FillOrderCard 填写订单卡密和卡号
func (s *sCamelOil) fillOrderCard(ctx context.Context, orderNo string, cardPassword string, cardNumber string) error {
// 1. 查询订单信息
var order *entity.V1CamelOilOrder
err := dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilOrder.Columns().OrderNo, orderNo).
Scan(&order)
if err != nil {
return gerror.Wrap(err, "查询订单失败")
}
if order == nil {
return gerror.New("订单不存在")
}
// 2. 更新卡密和卡号
_, err = dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilOrder.Columns().OrderNo, orderNo).
Update(&do.V1CamelOilOrder{
CardPassword: cardPassword,
CardNumber: cardNumber,
})
if err != nil {
return gerror.Wrap(err, "更新卡密失败")
}
// 3. 记录操作历史
remark := fmt.Sprintf("填写卡密和卡号")
_, err = dao.V1CamelOilOrderHistory.Ctx(ctx).DB(config.GetDatabaseV1()).Data(&do.V1CamelOilOrderHistory{
OrderNo: orderNo,
ChangeType: string(consts.CamelOilOrderChangeTypeFillCard),
AccountId: order.AccountId,
AccountName: order.AccountName,
Remark: remark,
}).Insert()
if err != nil {
g.Log().Errorf(ctx, "记录订单历史失败:%v", err)
}
return nil
}

View File

@@ -36,7 +36,7 @@ func (s *sCamelOil) updateCallbackResult(ctx context.Context, order *entity.V1Ca
return gerror.Wrap(err, "更新回调成功状态失败")
}
_ = s.RecordOrderHistory(ctx, order.OrderNo, "callback_success", "", historyDesc)
_ = s.RecordOrderHistory(ctx, order.OrderNo, consts.CamelOilOrderChangeTypeCallbackSuccess, "", historyDesc)
} else {
// 回调失败
notifyCount := order.NotifyCount + 1
@@ -54,7 +54,7 @@ func (s *sCamelOil) updateCallbackResult(ctx context.Context, order *entity.V1Ca
return gerror.Wrap(err, "更新回调失败状态失败")
}
_ = s.RecordOrderHistory(ctx, order.OrderNo, "callback_fail", "", historyDesc)
_ = s.RecordOrderHistory(ctx, order.OrderNo, consts.CamelOilOrderChangeTypeCallbackFail, "", historyDesc)
}
return nil

View File

@@ -9,7 +9,6 @@ import (
"kami/internal/model/do"
"kami/internal/model/entity"
"kami/utility/config"
"kami/utility/utils"
"github.com/gogf/gf/v2/os/glog"
)
@@ -46,7 +45,6 @@ func (s *sCamelOil) GetOrderHistory(ctx context.Context, req *v1.OrderHistoryReq
items := make([]v1.OrderHistoryItem, 0, len(histories))
for _, history := range histories {
items = append(items, v1.OrderHistoryItem{
HistoryUuid: history.HistoryUuid,
OrderNo: history.OrderNo,
ChangeType: consts.CamelOilOrderChangeType(history.ChangeType),
ChangeText: getOrderChangeTypeText(history.ChangeType),
@@ -66,15 +64,14 @@ func (s *sCamelOil) GetOrderHistory(ctx context.Context, req *v1.OrderHistoryReq
}
// RecordOrderHistory 记录订单历史
func (s *sCamelOil) RecordOrderHistory(ctx context.Context, orderNo, changeType, rawData, remark string) error {
func (s *sCamelOil) RecordOrderHistory(ctx context.Context, orderNo string, changeType consts.CamelOilOrderChangeType, rawData string, remark string) error {
m := dao.V1CamelOilOrderHistory.Ctx(ctx).DB(config.GetDatabaseV1())
_, err := m.Insert(&do.V1CamelOilOrderHistory{
HistoryUuid: utils.GenerateRandomUUID(),
OrderNo: orderNo,
ChangeType: changeType,
RawData: rawData,
Remark: remark,
OrderNo: orderNo,
ChangeType: string(changeType),
RawData: rawData,
Remark: remark,
})
if err != nil {

View File

@@ -0,0 +1,417 @@
package camel_oil
import (
"context"
"fmt"
"sync"
"sync/atomic"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gmlock"
"github.com/gogf/gf/v2/os/gtime"
"github.com/shopspring/decimal"
"kami/internal/consts"
"kami/internal/dao"
"kami/internal/model"
"kami/internal/model/do"
"kami/internal/model/entity"
"kami/utility/config"
"kami/utility/integration/camel_oil_api"
)
// ====================================================================================
// 预拉取订单管理相关方法
// ====================================================================================
// GetPrefetchOrderCapacity 获取当前可用订单容量
func (s *sCamelOil) GetPrefetchOrderCapacity(ctx context.Context) (capacity int, err error) {
m := dao.V1CamelOilPrefetchOrder.Ctx(ctx).DB(config.GetDatabaseV1())
count, err := m.Where(dao.V1CamelOilPrefetchOrder.Columns().Status, consts.CamelOilPrefetchOrderStatusPending).Count()
if err != nil {
return 0, gerror.Wrap(err, "查询预拉取订单库存失败")
}
return count, nil
}
// PrefetchOrderConcurrently 使用所有可用账号并发拉取订单,直到获取到可用订单为止
func (s *sCamelOil) PrefetchOrderConcurrently(ctx context.Context, amount float64) (result *model.PrefetchOrderResult, err error) {
// 1. 获取所有在线账号
m := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1())
var onlineAccounts []*entity.V1CamelOilAccount
err = m.Where(dao.V1CamelOilAccount.Columns().Status, consts.CamelOilAccountStatusOnline).Scan(&onlineAccounts)
if err != nil {
return nil, gerror.Wrap(err, "查询在线账号失败")
}
if len(onlineAccounts) == 0 {
return nil, gerror.New("暂无在线账号可用")
}
// 2. 使用控制并发量的信信道控制并发
concurrencyLimit := min(len(onlineAccounts), consts.CamelOilPrefetchMaxConcurrency)
var (
resultChan = make(chan *model.PrefetchOrderResult, 1)
errorChan = make(chan error, len(onlineAccounts))
semaphore = make(chan struct{}, concurrencyLimit) // 信信道控制并发量
wg sync.WaitGroup
mu sync.Mutex
found = false
)
// 3. 每个账号起一个协程尝试拉取,控制并发量
for _, account := range onlineAccounts {
acc := account // 避免闭包陷阱
wg.Go(func() {
// 获取信信道控制
semaphore <- struct{}{}
defer func() { <-semaphore }()
// 检查是否已经找到订单
mu.Lock()
if found {
mu.Unlock()
return
}
mu.Unlock()
// 拉取订单
platformOrderId, payUrl, err2 := camel_oil_api.NewClient().CreateOrder(ctx, acc.Phone, acc.Token, amount)
if err2 != nil {
if err2.Error() == "auth_error" {
_ = s.UpdateAccountStatus(ctx, acc.Id, consts.CamelOilAccountStatusInvalid, consts.CamelOilAccountChangeTypeInvalidate, "账号token失效")
}
errorChan <- err2
return
}
// 成功拉取订单,返回结果
mu.Lock()
if !found {
found = true
resultChan <- &model.PrefetchOrderResult{
PlatformOrderNo: platformOrderId,
AlipayUrl: payUrl,
AccountId: acc.Id,
AccountName: acc.AccountName,
Amount: amount,
}
}
mu.Unlock()
})
}
// 4. 等待所有协程完成並关闭Channel
go func() {
wg.Wait()
close(resultChan)
close(errorChan)
}()
// 5. 等待第一个成功的订单
if res := <-resultChan; res != nil {
glog.Infof(ctx, "并发拉取订单成功,账号=%s, 金额=%.2f", res.AccountName, res.Amount)
return res, nil
}
// 6. 所有账号都拉取失败
var lastErr error
for err = range errorChan {
lastErr = err
}
return nil, gerror.Wrap(lastErr, "所有账号拉取订单失败")
}
// 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)
if err != nil {
if err.Error() == "auth_error" {
_ = s.UpdateAccountStatus(ctx, account.Id, consts.CamelOilAccountStatusInvalid, consts.CamelOilAccountChangeTypeInvalidate, "账号token失效")
return 0, gerror.Wrap(err, "账号token失效拉取订单失败")
}
return 0, gerror.Wrap(err, "从骆驼平台拉取订单失败")
}
// 2. 计算过期时间
expireAt := gtime.Now().AddDate(0, 0, 1) // 24小时后过期
// 3. 保存预拉取订单记录
result, err := dao.V1CamelOilPrefetchOrder.Ctx(ctx).DB(config.GetDatabaseV1()).Insert(&do.V1CamelOilPrefetchOrder{
AccountId: account.Id,
AccountName: account.AccountName,
Amount: decimal.NewFromFloat(amount),
PlatformOrderNo: platformOrderId,
AlipayUrl: payUrl,
Status: int(consts.CamelOilPrefetchOrderStatusPending), // 待匹配
ExpireAt: expireAt,
})
if err != nil {
return 0, gerror.Wrap(err, "保存预拉取订单失败")
}
prefetchId, err = result.LastInsertId()
if err != nil {
return 0, gerror.Wrap(err, "获取预拉取订单ID失败")
}
// 4. 记录预拉取订单历史
_ = s.RecordPrefetchOrderHistory(ctx, prefetchId, consts.CamelOilPrefetchOrderChangeTypeFetch,
account.Id, account.AccountName, fmt.Sprintf("从骆驼平台拉取订单,平台订单号: %s", platformOrderId))
glog.Infof(ctx, "预拉取订单创建成功ID=%d, 账号=%s, 平台订单号=%s", prefetchId, account.Phone, platformOrderId)
return prefetchId, nil
}
// ConcurrentPrefetchOrders 使用多个账号并发拉取订单
func (s *sCamelOil) ConcurrentPrefetchOrders(ctx context.Context, amount float64, targetCount int) (successCount int, err error) {
// 1. 获取所有在线账号
m := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1())
var onlineAccounts []*entity.V1CamelOilAccount
err = m.Where(dao.V1CamelOilAccount.Columns().Status, consts.CamelOilAccountStatusOnline).Scan(&onlineAccounts)
if err != nil {
return 0, gerror.Wrap(err, "查询在线账号失败")
}
if len(onlineAccounts) == 0 {
return 0, gerror.New("暂无在线账号可用于拉取订单")
}
// 2. 使用协程池并发拉取
concurrencyLimit := consts.CamelOilPrefetchMaxConcurrency
if len(onlineAccounts) < concurrencyLimit {
concurrencyLimit = len(onlineAccounts)
}
var (
wg sync.WaitGroup
semaphore = make(chan struct{}, concurrencyLimit)
successCounter int32
accountIndex int32
accountMutex sync.Mutex
)
targetRemaining := targetCount
accountIndex = 0
for targetRemaining > 0 {
account := onlineAccounts[int(atomic.LoadInt32(&accountIndex))%len(onlineAccounts)]
atomic.AddInt32(&accountIndex, 1)
semaphore <- struct{}{} // 获取信号量
wg.Go(func() {
defer wg.Done()
defer func() { <-semaphore }() // 释放信号量
// 为了提高效率,每个账号可以拉取多单
for i := 0; i < 2 && targetRemaining > 0; i++ {
_, err := s.PrefetchOrder(ctx, account, amount)
if err != nil {
glog.Warningf(ctx, "账号%s拉取订单失败: %v", account.Phone, err)
continue
}
atomic.AddInt32(&successCounter, 1)
accountMutex.Lock()
targetRemaining--
accountMutex.Unlock()
}
})
}
wg.Wait()
successCount = int(atomic.LoadInt32(&successCounter))
glog.Infof(ctx, "并发拉取订单完成,目标: %d, 成功: %d", targetCount, successCount)
return successCount, nil
}
// SupplementPrefetchOrders 补充预拉取订单,当库存不足时调用
func (s *sCamelOil) SupplementPrefetchOrders(ctx context.Context) (supplementedCount int, err error) {
gmlock.Lock(consts.CamelOilPrefetchTaskLockKey)
defer gmlock.Unlock(consts.CamelOilPrefetchTaskLockKey)
// 1. 获取当前库存
capacity, err := s.GetPrefetchOrderCapacity(ctx)
if err != nil {
return 0, gerror.Wrap(err, "获取预拉取订单库存失败")
}
glog.Infof(ctx, "当前预拉取订单库存: %d", capacity)
// 2. 如果库存充足,无需补充
if capacity >= consts.CamelOilPrefetchOrderMinCapacity {
glog.Infof(ctx, "预拉取订单库存充足 (%d >= %d),无需补充", capacity, consts.CamelOilPrefetchOrderMinCapacity)
return 0, nil
}
// 3. 计算需要补充的数量
needCount := consts.CamelOilPrefetchOrderTargetCapacity - capacity
glog.Infof(ctx, "预拉取订单库存不足,需要补充 %d 单,金额: 100元", needCount)
// 4. 并发拉取订单
successCount, err := s.ConcurrentPrefetchOrders(ctx, 100, needCount)
if err != nil {
return 0, gerror.Wrap(err, "并发拉取订单失败")
}
return successCount, nil
}
// MatchPrefetchOrder 将预拉取订单与用户订单进行匹配
func (s *sCamelOil) MatchPrefetchOrder(ctx context.Context, orderId int64) (prefetchId int64, err error) {
// 1. 获取用户订单信息
var order *entity.V1CamelOilOrder
err = dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilOrder.Columns().Id, orderId).
Scan(&order)
if err != nil {
return 0, gerror.Wrap(err, "查询用户订单失败")
}
if order == nil {
return 0, gerror.New("用户订单不存在")
}
// 2. 查询待匹配的预拉取订单(同金额)
var prefetchOrder *entity.V1CamelOilPrefetchOrder
err = dao.V1CamelOilPrefetchOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilPrefetchOrder.Columns().Status, consts.CamelOilPrefetchOrderStatusPending).
Where(dao.V1CamelOilPrefetchOrder.Columns().Amount, order.Amount).
Where(dao.V1CamelOilPrefetchOrder.Columns().ExpireAt+">?", gtime.Now()). // 未过期
OrderAsc(dao.V1CamelOilPrefetchOrder.Columns().CreatedAt). // 优先选择最早的
Scan(&prefetchOrder)
if err != nil {
return 0, gerror.Wrap(err, "查询预拉取订单失败")
}
if prefetchOrder == nil {
return 0, gerror.New("暂无匹配的预拉取订单,请稍后重试")
}
// 3. 使用事务更新预拉取订单和用户订单
err = dao.V1CamelOilPrefetchOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
// 3.1 更新预拉取订单:标记为已匹配
_, err = dao.V1CamelOilPrefetchOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilPrefetchOrder.Columns().Id, prefetchOrder.Id).
Update(&do.V1CamelOilPrefetchOrder{
Status: int(consts.CamelOilPrefetchOrderStatusMatched),
OrderId: orderId,
MatchedAt: gtime.Now(),
})
if err != nil {
return gerror.Wrap(err, "更新预拉取订单状态失败")
}
// 3.2 更新用户订单:填充支付链接和平台订单号
_, err = dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilOrder.Columns().Id, orderId).
Update(&do.V1CamelOilOrder{
PlatformOrderNo: prefetchOrder.PlatformOrderNo,
AlipayUrl: prefetchOrder.AlipayUrl,
AccountId: prefetchOrder.AccountId,
AccountName: prefetchOrder.AccountName,
})
if err != nil {
return gerror.Wrap(err, "更新用户订单失败")
}
// 3.3 记录预拉取订单历史
_ = s.RecordPrefetchOrderHistory(ctx, prefetchOrder.Id, consts.CamelOilPrefetchOrderChangeTypeMatch,
prefetchOrder.AccountId, prefetchOrder.AccountName,
fmt.Sprintf("与用户订单匹配订单ID=%d", orderId))
// 3.4 记录用户订单历史
_ = s.RecordOrderHistory(ctx, order.OrderNo, consts.CamelOilOrderChangeTypeGetPayUrl,
fmt.Sprintf("预拉取订单ID=%d", prefetchOrder.Id),
fmt.Sprintf("从预拉取订单获取支付链接,平台订单号=%s", prefetchOrder.PlatformOrderNo))
return nil
})
if err != nil {
return 0, err
}
prefetchId = prefetchOrder.Id
glog.Infof(ctx, "预拉取订单匹配成功预拉取ID=%d, 用户订单ID=%d, 平台订单号=%s",
prefetchId, orderId, prefetchOrder.PlatformOrderNo)
return prefetchId, nil
}
// RecordPrefetchOrderHistory 记录预拉取订单历史
func (s *sCamelOil) RecordPrefetchOrderHistory(ctx context.Context, prefetchId int64,
changeType consts.CamelOilPrefetchOrderChangeType, accountId int64, accountName, remark string) error {
_, err := dao.V1CamelOilPrefetchOrderHistory.Ctx(ctx).DB(config.GetDatabaseV1()).Insert(&do.V1CamelOilPrefetchOrderHistory{
PrefetchId: prefetchId,
ChangeType: string(changeType),
AccountId: accountId,
AccountName: accountName,
Remark: remark,
})
if err != nil {
glog.Errorf(ctx, "记录预拉取订单历史失败: %v", err)
}
return err
}
// CleanExpiredPrefetchOrders 清理过期的预拉取订单
func (s *sCamelOil) CleanExpiredPrefetchOrders(ctx context.Context) (cleanedCount int, err error) {
m := dao.V1CamelOilPrefetchOrder.Ctx(ctx).DB(config.GetDatabaseV1())
// 查询已过期的待匹配订单
var expiredOrders []*entity.V1CamelOilPrefetchOrder
err = m.Where(dao.V1CamelOilPrefetchOrder.Columns().Status, consts.CamelOilPrefetchOrderStatusPending).
Where(dao.V1CamelOilPrefetchOrder.Columns().ExpireAt+"<?", gtime.Now()).
Scan(&expiredOrders)
if err != nil {
return 0, gerror.Wrap(err, "查询过期订单失败")
}
if len(expiredOrders) == 0 {
return 0, nil
}
// 标记为已过期
for _, order := range expiredOrders {
_, err = m.Where(dao.V1CamelOilPrefetchOrder.Columns().Id, order.Id).
Update(&do.V1CamelOilPrefetchOrder{
Status: int(consts.CamelOilPrefetchOrderStatusExpired),
})
if err != nil {
glog.Warningf(ctx, "标记预拉取订单为过期失败ID=%d: %v", order.Id, err)
continue
}
// 记录历史
_ = s.RecordPrefetchOrderHistory(ctx, order.Id, consts.CamelOilPrefetchOrderChangeTypeExpire,
order.AccountId, order.AccountName, "订单已过期,自动标记失效")
cleanedCount++
}
glog.Infof(ctx, "清理过期预拉取订单完成,清理数量: %d", cleanedCount)
return cleanedCount, nil
}

View File

@@ -3,9 +3,6 @@ package card_apple_account
import (
"testing"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
"kami/api/commonApi"
_ "kami/internal/logic/sys_user_payment"
"kami/internal/model"
@@ -14,9 +11,7 @@ import (
_ "github.com/gogf/gf/contrib/nosql/redis/v2"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/test/gtest"
"github.com/shopspring/decimal"
)
func Test_sAppleAccount_AddWalletAmount(t *testing.T) {
@@ -38,21 +33,21 @@ func Test_sAppleAccount_IsUnscopedTodayRepeatedByName(t *testing.T) {
a.IsUnscopedTodayRepeatedByName(gctx.New(), "123456")
}
func Test_sAppleAccount_GetAccordingAccount(t *testing.T) {
ctx := gctx.New()
result, err := New().GetAccordingAccount(ctx, "123456", decimal.NewFromInt(100))
if err != nil {
glog.Error(ctx, err)
}
glog.Info(ctx, "当前账号", result)
}
// func Test_sAppleAccount_GetAccordingAccount(t *testing.T) {
// ctx := gctx.New()
// result, err := New().GetAccordingAccount(ctx, "123456", decimal.NewFromInt(100))
// if err != nil {
// glog.Error(ctx, err)
// }
// glog.Info(ctx, "当前账号", result)
// }
func Test_sAppleAccount_GetAccordingAccountV2(t *testing.T) {
g.Cfg().GetAdapter().(*gcfg.AdapterFile).SetFileName("/manifest/config/config.yaml")
ctx := gctx.New()
data, err := New().GetAccordingAccountV2(ctx, "123456", decimal.NewFromInt(100))
if err != nil {
glog.Error(ctx, err)
}
glog.Info(ctx, "当前账号", data)
}
// func Test_sAppleAccount_GetAccordingAccountV2(t *testing.T) {
// g.Cfg().GetAdapter().(*gcfg.AdapterFile).SetFileName("/manifest/config/config.yaml")
// ctx := gctx.New()
// data, err := New().GetAccordingAccountV2(ctx, "123456", decimal.NewFromInt(100))
// if err != nil {
// glog.Error(ctx, err)
// }
// glog.Info(ctx, "当前账号", data)
// }

View File

@@ -0,0 +1,33 @@
package card_apple_account
import (
"context"
"github.com/gogf/gf/v2/os/glog"
"kami/internal/consts"
"kami/internal/dao"
"kami/internal/model/entity"
"kami/utility/config"
"kami/utility/integration/apple"
)
func (a *sAppleAccount) CronHealthCheck(ctx context.Context) {
m := dao.V1CardAppleAccountInfo.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1CardAppleAccountInfo.Columns().Status, consts.AppleAccountNormal)
count, _ := m.Count()
for i := 0; i < int(count); i++ {
data := make(map[string]*entity.V1CardAppleAccountInfo)
_ = m.Page(i+1, 100).Scan(&data)
for _, accountInfo := range data {
resp, err := apple.NewClient().Heartbeat(ctx, &apple.HeartBeatReq{
Account: accountInfo.Account,
Password: accountInfo.Password,
OrderId: accountInfo.Id,
})
if err != nil {
glog.Error(ctx, "请求错误", err)
}
if resp.Data.Status != 0 {
_ = a.ModifyStatus(ctx, accountInfo.Id, consts.AppleAccountWrongPassword, nil)
}
}
}
}

View File

@@ -0,0 +1 @@
package card_apple_order

View File

@@ -24,15 +24,15 @@ import (
)
// handleRedeemResult 处理核销结果,根据状态码分类进行对应处理
func (h *sAppleOrder) handleRedeemResult(ctx context.Context, orderEntity *entity.V1CardAppleRechargeInfo, accountInfo *entity.V1CardAppleAccountInfo) error {
func (h *sAppleOrder) handleRedeemResult(ctx context.Context, orderInfo *entity.V1CardAppleRechargeInfo, accountInfo *entity.V1CardAppleAccountInfo) error {
// 调用 Apple 服务进行核销(同步等待)
redeemClient := apple.NewClient()
// 准备推送请求
redeemReq := &apple.RedeemReq{
Account: accountInfo.Account,
Password: accountInfo.Password,
OrderId: orderEntity.OrderNo,
RedemptionCode: orderEntity.CardPass,
OrderId: orderInfo.OrderNo,
RedemptionCode: orderInfo.CardPass,
}
resp, err := redeemClient.Redeem(ctx, redeemReq)
if err != nil {
@@ -43,27 +43,27 @@ func (h *sAppleOrder) handleRedeemResult(ctx context.Context, orderEntity *entit
switch {
// 1. 成功状态CodeSuccess = 0
case resp.Code == apple.CodeSuccess:
return h.handleRedeemSuccess(ctx, orderEntity, accountInfo, resp.Data.Amount, resp.Data.BalanceBefore, resp.Data.BalanceAfter)
return h.handleRedeemSuccess(ctx, orderInfo, accountInfo, resp.Data.Amount, resp.Data.BalanceBefore, resp.Data.BalanceAfter)
// 2. 网络请求或系统资源错误5000-5999
case resp.Code >= 5000 && resp.Code < 6000:
return h.handleSystemError(ctx, orderEntity, accountInfo, resp.Code, resp.Message)
return h.handleSystemError(ctx, orderInfo, accountInfo, resp.Code, resp.Message)
// 3. 苹果账户原因错误8001-8005
case resp.Code >= 8001 && resp.Code <= 8005:
return h.handleAccountError(ctx, orderEntity, accountInfo, resp.Code, resp.Message)
return h.handleAccountError(ctx, orderInfo, accountInfo, resp.Code, resp.Message)
// 4. 充值限制错误8010-8012
case resp.Code >= 8010 && resp.Code <= 8012:
return h.handleRedeemLimitError(ctx, orderEntity, accountInfo, resp.Code, resp.Message)
return h.handleRedeemLimitError(ctx, orderInfo, accountInfo, resp.Code, resp.Message)
// 5. 卡密错误8013-8014
case resp.Code >= 8013 && resp.Code <= 8014:
return h.handleCardCodeError(ctx, orderEntity, accountInfo, resp.Code, resp.Message)
return h.handleCardCodeError(ctx, orderInfo, accountInfo, resp.Code, resp.Message)
// 未知错误
default:
return h.handleRedeemFailed(ctx, orderEntity, accountInfo, resp.Message)
return h.handleRedeemFailed(ctx, orderInfo, accountInfo, resp.Message)
}
}

View File

@@ -0,0 +1,10 @@
package model
// PrefetchOrderResult 预拉取订单结果
type PrefetchOrderResult struct {
PlatformOrderNo string // 骆驼平台订单号
AlipayUrl string // 支付宝支付链接
AccountId int64 // 账号ID
AccountName string // 账号名称
Amount float64 // 订单金额
}

View File

@@ -13,7 +13,6 @@ import (
type V1CamelOilAccountHistory struct {
g.Meta `orm:"table:camel_oil_account_history, do:true"`
Id any // 主键ID
HistoryUuid any // 历史记录唯一标识
AccountId any // 账号ID
ChangeType any // 变更类型create/login/offline/login_fail/pause/resume/invalidate/order_bind/order_complete/update/delete
StatusBefore any // 变更前状态

View File

@@ -20,6 +20,8 @@ type V1CamelOilOrder struct {
PlatformOrderNo any // 骆驼平台订单号
Amount any // 订单金额
AlipayUrl any // 支付宝支付链接
CardPassword any // 卡密
CardNumber any // 卡号
Status any // 状态1待支付 2已支付 3支付超时 4下单失败
PayStatus any // 支付状态0未支付 1已支付 2超时
NotifyStatus any // 回调状态0未回调 1已回调 2回调失败

View File

@@ -13,7 +13,6 @@ import (
type V1CamelOilOrderHistory struct {
g.Meta `orm:"table:camel_oil_order_history, do:true"`
Id any // 主键ID
HistoryUuid any // 历史记录唯一标识
OrderNo any // 订单号
ChangeType any // 变更类型create/submit/get_pay_url/check_pay/paid/timeout/fail/callback_success/callback_fail
AccountId any // 关联账号ID

View File

@@ -0,0 +1,30 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// V1CamelOilPrefetchOrder is the golang structure of table camel_oil_prefetch_order for DAO operations like Where/Data.
type V1CamelOilPrefetchOrder struct {
g.Meta `orm:"table:camel_oil_prefetch_order, do:true"`
Id any // 主键ID
AccountId any // 拉取时使用的账号ID
AccountName any // 账号名称
Amount any // 预拉取订单金额
PlatformOrderNo any // 骆驼平台订单号
AlipayUrl any // 支付宝支付链接
Status any // 预拉取订单状态1待匹配 2已匹配 3已过期 4已失效
OrderId any // 匹配后的订单ID关联camel_oil_order表
MatchedAt *gtime.Time // 匹配时间
ExpireAt *gtime.Time // 预拉取订单过期时间通常为24小时后
FailureReason any // 失败原因
Remark any // 备注信息
CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间
DeletedAt *gtime.Time // 删除时间(软删除)
}

View File

@@ -0,0 +1,25 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// V1CamelOilPrefetchOrderHistory is the golang structure of table camel_oil_prefetch_order_history for DAO operations like Where/Data.
type V1CamelOilPrefetchOrderHistory struct {
g.Meta `orm:"table:camel_oil_prefetch_order_history, do:true"`
Id any // 主键ID
PrefetchId any // 预拉取订单ID
ChangeType any // 变更类型create/fetch/match/expire/invalidate
AccountId any // 关联账号ID
AccountName any // 账号名称
RawData any // 原始响应数据
Remark any // 备注
CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间
DeletedAt *gtime.Time // 删除时间(软删除)
}

View File

@@ -11,7 +11,6 @@ import (
// V1CamelOilAccountHistory is the golang structure for table v1camel_oil_account_history.
type V1CamelOilAccountHistory struct {
Id int64 `json:"id" orm:"id" description:"主键ID"`
HistoryUuid string `json:"historyUuid" orm:"history_uuid" description:"历史记录唯一标识"`
AccountId int64 `json:"accountId" orm:"account_id" description:"账号ID"`
ChangeType string `json:"changeType" orm:"change_type" description:"变更类型create/login/offline/login_fail/pause/resume/invalidate/order_bind/order_complete/update/delete"`
StatusBefore int `json:"statusBefore" orm:"status_before" description:"变更前状态"`

View File

@@ -19,6 +19,8 @@ type V1CamelOilOrder struct {
PlatformOrderNo string `json:"platformOrderNo" orm:"platform_order_no" description:"骆驼平台订单号"`
Amount decimal.Decimal `json:"amount" orm:"amount" description:"订单金额"`
AlipayUrl string `json:"alipayUrl" orm:"alipay_url" description:"支付宝支付链接"`
CardPassword string `json:"cardPassword" orm:"card_password" description:"卡密"`
CardNumber string `json:"cardNumber" orm:"card_number" description:"卡号"`
Status int `json:"status" orm:"status" description:"状态1待支付 2已支付 3支付超时 4下单失败"`
PayStatus int `json:"payStatus" orm:"pay_status" description:"支付状态0未支付 1已支付 2超时"`
NotifyStatus int `json:"notifyStatus" orm:"notify_status" description:"回调状态0未回调 1已回调 2回调失败"`

View File

@@ -11,7 +11,6 @@ import (
// V1CamelOilOrderHistory is the golang structure for table v1camel_oil_order_history.
type V1CamelOilOrderHistory struct {
Id int64 `json:"id" orm:"id" description:"主键ID"`
HistoryUuid string `json:"historyUuid" orm:"history_uuid" description:"历史记录唯一标识"`
OrderNo string `json:"orderNo" orm:"order_no" description:"订单号"`
ChangeType string `json:"changeType" orm:"change_type" description:"变更类型create/submit/get_pay_url/check_pay/paid/timeout/fail/callback_success/callback_fail"`
AccountId int64 `json:"accountId" orm:"account_id" description:"关联账号ID"`

View File

@@ -0,0 +1,29 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
"github.com/shopspring/decimal"
)
// V1CamelOilPrefetchOrder is the golang structure for table v1camel_oil_prefetch_order.
type V1CamelOilPrefetchOrder struct {
Id int64 `json:"id" orm:"id" description:"主键ID"`
AccountId int64 `json:"accountId" orm:"account_id" description:"拉取时使用的账号ID"`
AccountName string `json:"accountName" orm:"account_name" description:"账号名称"`
Amount decimal.Decimal `json:"amount" orm:"amount" description:"预拉取订单金额"`
PlatformOrderNo string `json:"platformOrderNo" orm:"platform_order_no" description:"骆驼平台订单号"`
AlipayUrl string `json:"alipayUrl" orm:"alipay_url" description:"支付宝支付链接"`
Status int `json:"status" orm:"status" description:"预拉取订单状态1待匹配 2已匹配 3已过期 4已失效"`
OrderId int64 `json:"orderId" orm:"order_id" description:"匹配后的订单ID关联camel_oil_order表"`
MatchedAt *gtime.Time `json:"matchedAt" orm:"matched_at" description:"匹配时间"`
ExpireAt *gtime.Time `json:"expireAt" orm:"expire_at" description:"预拉取订单过期时间通常为24小时后"`
FailureReason string `json:"failureReason" orm:"failure_reason" description:"失败原因"`
Remark string `json:"remark" orm:"remark" description:"备注信息"`
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"`
DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:"删除时间(软删除)"`
}

View File

@@ -0,0 +1,23 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
)
// V1CamelOilPrefetchOrderHistory is the golang structure for table v1camel_oil_prefetch_order_history.
type V1CamelOilPrefetchOrderHistory struct {
Id int64 `json:"id" orm:"id" description:"主键ID"`
PrefetchId int64 `json:"prefetchId" orm:"prefetch_id" description:"预拉取订单ID"`
ChangeType string `json:"changeType" orm:"change_type" description:"变更类型create/fetch/match/expire/invalidate"`
AccountId int64 `json:"accountId" orm:"account_id" description:"关联账号ID"`
AccountName string `json:"accountName" orm:"account_name" description:"账号名称"`
RawData string `json:"rawData" orm:"raw_data" description:"原始响应数据"`
Remark string `json:"remark" orm:"remark" description:"备注"`
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"`
DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:"删除时间(软删除)"`
}

View File

@@ -9,6 +9,7 @@ import (
"context"
v1 "kami/api/camel_oil/v1"
"kami/internal/consts"
"kami/internal/model"
"kami/internal/model/entity"
)
@@ -35,9 +36,6 @@ type (
// GetAvailableOrderCapacity 获取当前可用订单容量
// 计算所有在线账号的剩余可下单数之和
GetAvailableOrderCapacity(ctx context.Context) (capacity int, err error)
// CheckAndTriggerAccountLogin 检查容量并触发账号登录
// 如果可用订单容量<50,触发账号登录任务
CheckAndTriggerAccountLogin(ctx context.Context) (err error)
// GetAccountPoolStatus 获取账号池状态统计
GetAccountPoolStatus(ctx context.Context) (status map[string]interface{}, err error)
// GetAccountHistory 获取账号历史记录
@@ -46,10 +44,7 @@ type (
// 注意:当前使用假数据,实际应对接骆驼加油平台和接码平台
LoginAccount(ctx context.Context) (err error)
// BatchLoginAccounts 批量登录账号
BatchLoginAccounts(ctx context.Context, count int) (successCount int, err error)
// CheckAndLoginAccounts 检查容量并登录账号
// 根据当前可用订单容量,决定是否需要登录新账号
CheckAndLoginAccounts(ctx context.Context) (err error)
BatchLoginAccounts(ctx context.Context, count int64) (successCount int64, err error)
// GetAvailableAccount 获取可用账号按last_used_at轮询
// 选择条件:
// 1. status=2在线
@@ -59,8 +54,12 @@ type (
GetAvailableAccount(ctx context.Context) (account *entity.V1CamelOilAccount, err error)
// GetAccountStatistics 获取账号统计信息
GetAccountStatistics(ctx context.Context, req *v1.AccountStatisticsReq) (res *v1.AccountStatisticsRes, err error)
// CronAccountLoginTask 账号登录任务 - 由cron调度器定期调用
CronAccountLoginTask(ctx context.Context) error
// CronAccountPrefetchTask 账户预拉取定时任务 - 由cron调度器定期调用
// 流程:并发拉取账户到指定数量
CronAccountPrefetchTask(ctx context.Context) error
// CronPrefetchOrderSupplementTask 预拉取订单补充定时任务 - 由cron调度器定期调用
// 流程:检查预拉取订单库存,不足时补充
CronPrefetchOrderSupplementTask(ctx context.Context) error
// CronOrderPaymentCheckTask 订单支付状态检测任务 - 由cron调度器定期调用
CronOrderPaymentCheckTask(ctx context.Context) error
// CronAccountDailyResetTask 账号日重置任务 - 由cron调度器在每日00:05调用
@@ -68,6 +67,8 @@ type (
CronVerifyCodeCheckTask(ctx context.Context) error
// SubmitOrder 提交订单并返回支付宝支付链接
SubmitOrder(ctx context.Context, req *v1.SubmitOrderReq) (res *v1.SubmitOrderRes, err error)
// UpdateOrderStatus 更新订单状态并记录历史
UpdateOrderStatus(ctx context.Context, orderId int64, newStatus consts.CamelOilOrderStatus, operationType consts.CamelOilOrderChangeType, rawData string, description string) (err error)
// TriggerOrderCallback 触发订单回调
TriggerOrderCallback(ctx context.Context, req *v1.OrderCallbackReq) (res *v1.OrderCallbackRes, err error)
// ProcessPendingCallbacks 处理待回调订单(定时任务使用)
@@ -75,13 +76,29 @@ type (
// GetOrderHistory 获取订单历史记录
GetOrderHistory(ctx context.Context, req *v1.OrderHistoryReq) (res *v1.OrderHistoryRes, err error)
// RecordOrderHistory 记录订单历史
RecordOrderHistory(ctx context.Context, orderNo string, changeType string, rawData string, remark string) error
RecordOrderHistory(ctx context.Context, orderNo string, changeType consts.CamelOilOrderChangeType, rawData string, remark string) error
// GetAccountOrders 查询账号关联订单
GetAccountOrders(ctx context.Context, req *v1.AccountOrderListReq) (res *v1.AccountOrderListRes, err error)
// ListOrder 查询订单列表
ListOrder(ctx context.Context, req *v1.ListOrderReq) (res *v1.ListOrderRes, err error)
// OrderDetail 查询订单详情
OrderDetail(ctx context.Context, req *v1.OrderDetailReq) (res *v1.OrderDetailRes, err error)
// GetPrefetchOrderCapacity 获取当前可用订单容量
GetPrefetchOrderCapacity(ctx context.Context) (capacity int, err error)
// PrefetchOrderConcurrently 使用所有可用账号并发拉取订单,直到获取到可用订单为止
PrefetchOrderConcurrently(ctx context.Context, amount float64) (result *model.PrefetchOrderResult, err error)
// PrefetchOrder 拉取单个订单(用于单个账号)
PrefetchOrder(ctx context.Context, account *entity.V1CamelOilAccount, amount float64) (prefetchId int64, err error)
// ConcurrentPrefetchOrders 使用多个账号并发拉取订单
ConcurrentPrefetchOrders(ctx context.Context, amount float64, targetCount int) (successCount int, err error)
// SupplementPrefetchOrders 补充预拉取订单,当库存不足时调用
SupplementPrefetchOrders(ctx context.Context) (supplementedCount int, err error)
// MatchPrefetchOrder 将预拉取订单与用户订单进行匹配
MatchPrefetchOrder(ctx context.Context, orderId int64) (prefetchId int64, err error)
// RecordPrefetchOrderHistory 记录预拉取订单历史
RecordPrefetchOrderHistory(ctx context.Context, prefetchId int64, changeType consts.CamelOilPrefetchOrderChangeType, accountId int64, accountName string, remark string) error
// CleanExpiredPrefetchOrders 清理过期的预拉取订单
CleanExpiredPrefetchOrders(ctx context.Context) (cleanedCount int, err error)
}
)

View File

@@ -58,6 +58,7 @@ type (
GetExcludeAccounts(ctx context.Context, machineId string) (m []*model.AccountIdInfo, err error)
// ClearCurrentTargetAccount 清空缓存
ClearCurrentTargetAccount(ctx context.Context, machineId string) (err error)
CronHealthCheck(ctx context.Context)
// GetHistoryOneByOrderNo 根据ID获取账号历史
GetHistoryOneByOrderNo(ctx context.Context, accountId string, orderNo string) (data *entity.V1CardAppleAccountInfoHistory, err error)
// GetAccordingAccountV3 账户分配算法,适合多线程

View File

@@ -43,6 +43,8 @@ CREATE TABLE `camel_oil_order` (
`platform_order_no` varchar(128) DEFAULT NULL COMMENT '骆驼平台订单号',
`amount` decimal(10,2) NOT NULL COMMENT '订单金额',
`alipay_url` text DEFAULT NULL COMMENT '支付宝支付链接',
`card_password` varchar(256) DEFAULT NULL COMMENT '卡密',
`card_number` varchar(256) DEFAULT NULL COMMENT '卡号',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态1待支付 2已支付 3支付超时 4下单失败',
`pay_status` tinyint NOT NULL DEFAULT 0 COMMENT '支付状态0未支付 1已支付 2超时',
`notify_status` tinyint NOT NULL DEFAULT 0 COMMENT '回调状态0未回调 1已回调 2回调失败',
@@ -65,6 +67,8 @@ CREATE TABLE `camel_oil_order` (
KEY `idx_notify_status` (`notify_status`),
KEY `idx_created_at` (`created_at`),
KEY `idx_account_status` (`account_id`, `status`),
KEY `idx_card_password` (`card_password`),
KEY `idx_card_number` (`card_number`),
KEY `idx_deleted_at` (`deleted_at`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '骆驼加油订单表';
@@ -73,7 +77,6 @@ DROP TABLE IF EXISTS `camel_oil_account_history`;
CREATE TABLE `camel_oil_account_history` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`history_uuid` varchar(36) NOT NULL COMMENT '历史记录唯一标识',
`account_id` bigint NOT NULL COMMENT '账号ID',
`change_type` varchar(32) NOT NULL COMMENT '变更类型create/login/offline/login_fail/pause/resume/invalidate/order_bind/order_complete/update/delete',
`status_before` tinyint DEFAULT NULL COMMENT '变更前状态',
@@ -84,7 +87,6 @@ CREATE TABLE `camel_oil_account_history` (
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间软删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_history_uuid` (`history_uuid`),
KEY `idx_account_id` (`account_id`),
CONSTRAINT `fk_camel_oil_account_history_account_id` FOREIGN KEY (`account_id`) REFERENCES `camel_oil_account` (`id`) ON DELETE CASCADE,
KEY `idx_change_type` (`change_type`),
@@ -97,7 +99,6 @@ DROP TABLE IF EXISTS `camel_oil_order_history`;
CREATE TABLE `camel_oil_order_history` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`history_uuid` varchar(36) NOT NULL COMMENT '历史记录唯一标识',
`order_no` varchar(64) NOT NULL COMMENT '订单号',
`change_type` varchar(32) NOT NULL COMMENT '变更类型create/submit/get_pay_url/check_pay/paid/timeout/fail/callback_success/callback_fail',
`account_id` bigint DEFAULT NULL COMMENT '关联账号ID',
@@ -108,7 +109,6 @@ CREATE TABLE `camel_oil_order_history` (
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间软删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_history_uuid` (`history_uuid`),
KEY `idx_order_no` (`order_no`),
KEY `idx_account_id` (`account_id`),
CONSTRAINT `fk_camel_oil_order_history_account_id` FOREIGN KEY (`account_id`) REFERENCES `camel_oil_account` (`id`) ON DELETE SET NULL,
@@ -116,3 +116,58 @@ CREATE TABLE `camel_oil_order_history` (
KEY `idx_created_at` (`created_at`),
KEY `idx_deleted_at` (`deleted_at`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '骆驼加油订单历史表';
-- 5. 骆驼加油预拉取订单表
DROP TABLE IF EXISTS `camel_oil_prefetch_order`;
CREATE TABLE `camel_oil_prefetch_order` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`account_id` bigint NOT NULL COMMENT '拉取时使用的账号ID',
`account_name` varchar(128) DEFAULT NULL COMMENT '账号名称',
`amount` decimal(10,2) NOT NULL COMMENT '预拉取订单金额',
`platform_order_no` varchar(128) DEFAULT NULL COMMENT '骆驼平台订单号',
`alipay_url` text DEFAULT NULL COMMENT '支付宝支付链接',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '预拉取订单状态1待匹配 2已匹配 3已过期 4已失效',
`order_id` bigint DEFAULT NULL COMMENT '匹配后的订单ID关联camel_oil_order表',
`matched_at` datetime DEFAULT NULL COMMENT '匹配时间',
`expire_at` datetime DEFAULT NULL COMMENT '预拉取订单过期时间通常为24小时后',
`failure_reason` text DEFAULT NULL COMMENT '失败原因',
`remark` text DEFAULT NULL COMMENT '备注信息',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间软删除',
PRIMARY KEY (`id`),
KEY `idx_account_id` (`account_id`),
CONSTRAINT `fk_camel_oil_prefetch_order_account_id` FOREIGN KEY (`account_id`) REFERENCES `camel_oil_account` (`id`) ON DELETE RESTRICT,
KEY `idx_status` (`status`),
KEY `idx_status_expire` (`status`, `expire_at`),
KEY `idx_order_id` (`order_id`),
CONSTRAINT `fk_camel_oil_prefetch_order_order_id` FOREIGN KEY (`order_id`) REFERENCES `camel_oil_order` (`id`) ON DELETE SET NULL,
KEY `idx_platform_order_no` (`platform_order_no`),
KEY `idx_created_at` (`created_at`),
KEY `idx_deleted_at` (`deleted_at`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '骆驼加油预拉取订单表';
-- 6. 骆驼加油预拉取订单历史表
DROP TABLE IF EXISTS `camel_oil_prefetch_order_history`;
CREATE TABLE `camel_oil_prefetch_order_history` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`prefetch_id` bigint NOT NULL COMMENT '预拉取订单ID',
`change_type` varchar(32) NOT NULL COMMENT '变更类型create/fetch/match/expire/invalidate',
`account_id` bigint DEFAULT NULL COMMENT '关联账号ID',
`account_name` varchar(128) DEFAULT NULL COMMENT '账号名称',
`raw_data` text DEFAULT NULL COMMENT '原始响应数据',
`remark` text DEFAULT NULL COMMENT '备注',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间软删除',
PRIMARY KEY (`id`),
KEY `idx_prefetch_id` (`prefetch_id`),
CONSTRAINT `fk_camel_oil_prefetch_order_history_prefetch_id` FOREIGN KEY (`prefetch_id`) REFERENCES `camel_oil_prefetch_order` (`id`) ON DELETE CASCADE,
KEY `idx_account_id` (`account_id`),
CONSTRAINT `fk_camel_oil_prefetch_order_history_account_id` FOREIGN KEY (`account_id`) REFERENCES `camel_oil_account` (`id`) ON DELETE SET NULL,
KEY `idx_change_type` (`change_type`),
KEY `idx_created_at` (`created_at`),
KEY `idx_deleted_at` (`deleted_at`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '骆驼加油预拉取订单历史表';

View File

@@ -12,6 +12,13 @@ import (
// Register 注册定时任务
func Register(ctx context.Context) {
//registerMainTasks(ctx)
// 骆驼加油模块定时任务
registerCamelOilTasks(ctx)
}
func registerMainTasks(ctx context.Context) {
//每日0时执行
_, _ = gcron.AddSingleton(ctx, "0 0 0 * * ?", func(ctx context.Context) {
tracer := gtrace.NewTracer("每日0时定时任务")
@@ -65,31 +72,24 @@ func Register(ctx context.Context) {
glog.Error(ctx, "京东支付状态监控任务失败", err)
}
})
// 骆驼加油模块定时任务
//registerCamelOilTasks(ctx)
}
// registerCamelOilTasks 注册骆驼加油模块的定时任务
func registerCamelOilTasks(ctx context.Context) {
// 账号登录任务 - 每5分钟执行
_, _ = gcron.AddSingleton(ctx, "@every 5m", func(ctx context.Context) {
_ = service.CamelOil().CronAccountLoginTask(ctx)
}, "CamelOilAccountLogin")
// 账户预拉取任务 - 每30秒执行一次
// 流程:并发拉取账户到指定数量
//_, _ = gcron.AddSingleton(ctx, "@every 10s", func(ctx context.Context) {
// _ = service.CamelOil().CronAccountPrefetchTask(ctx)
//}, "CamelOilAccountPrefetch")
//
//_, _ = gcron.AddSingleton(ctx, "@every 10s", func(ctx context.Context) {
// _ = service.CamelOil().CronVerifyCodeCheckTask(ctx)
//}, "CamelOilAccountVerifyCodeCheck")
_, _ = gcron.AddSingleton(ctx, "@every 5s", func(ctx context.Context) {
_ = service.CamelOil().CronVerifyCodeCheckTask(ctx)
}, "CamelOilVerifyCodeCheck")
// 订单支付状态检测任务 - 每1分钟执行
_, _ = gcron.AddSingleton(ctx, "@every 10s", func(ctx context.Context) {
_ = service.CamelOil().CronOrderPaymentCheckTask(ctx)
}, "CamelOilOrderPaymentCheck")
// 账号日重置任务 - 每日00:00执行
_, _ = gcron.AddSingleton(ctx, "0 0 * * *", func(ctx context.Context) {
_ = service.CamelOil().CronAccountDailyResetTask(ctx)
}, "CamelOilAccountDailyReset")
//// 流程:检查预拉取订单库存,不足时补充
//_, _ = gcron.AddSingleton(ctx, "@every 10s", func(ctx context.Context) {
// _ = service.CamelOil().CronPrefetchOrderSupplementTask(ctx)
//}, "CamelOilPrefetchOrderSupplement")
glog.Info(ctx, "骆驼加油模块定时任务注册完成")
}

View File

@@ -6,6 +6,8 @@ import (
"errors"
"github.com/gogf/gf/v2/net/gclient"
"github.com/gogf/gf/v2/os/glog"
"github.com/google/uuid"
"strings"
)
type Client struct {
@@ -20,25 +22,23 @@ func NewClient() *Client {
func (c *Client) SendCaptcha(ctx context.Context, phone string) (bool, error) {
req := struct {
OpenId string `json:"openId"`
Phone string `json:"phone"`
CouponStatus string `json:"couponStatus"`
Channel string `json:"channel"`
Phone string `json:"phone"`
Channel string `json:"channel"`
}{
OpenId: "app2511181557205741495",
Phone: phone,
CouponStatus: "unused",
Channel: "app",
Phone: phone,
Channel: "app",
}
resp, err := c.Client.Post(ctx, "https://recharge3.bac365.com/camel_wechat_mini_oil_server/refueling/getUserCouponList", req)
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(resp.ReadAll(), &respStruct)
err = json.Unmarshal([]byte(respStr), &respStruct)
return respStruct.Code == "success", err
}
@@ -52,11 +52,12 @@ func (c *Client) LoginWithCaptcha(ctx context.Context, phone string, code string
Codes: code,
Channel: "app",
}
resp, err := c.Client.Post(ctx, "https://recharge3.bac365.com/camel_wechat_mini_oil_server/loginApp", req)
resp, err := c.Client.ContentJson().Post(ctx, "https://recharge3.bac365.com/camel_wechat_mini_oil_server/loginApp", req)
if err != nil {
return "", err
}
glog.Info(ctx, "登录", req, resp.ReadAllString())
respStr := resp.ReadAllString()
glog.Info(ctx, "登录", req, respStr)
respStruct := struct {
LoginUser struct {
UserIdApp string `json:"userIdApp"`
@@ -65,18 +66,31 @@ func (c *Client) LoginWithCaptcha(ctx context.Context, phone string, code string
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"`
Token string `json:"token,omitempty"`
}{}
err = json.Unmarshal(resp.ReadAll(), &respStruct)
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", token)
resp, err := c.Client.Post(ctx, "https://recharge3.bac365.com/camel_wechat_mini_oil_server/eCardMall/wechatCardGoods", struct {
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",
@@ -109,22 +123,60 @@ func (c *Client) CreateOrder(ctx context.Context, phone, token string, amount fl
if goodId == "" {
return "", "", errors.New("当前金额不支持")
}
//遍历 100次
for i := 0; i < 100; i++ {
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: "app2511181557205741495",
Phone: phone,
GoodId: goodId,
GoodNum: 1,
BindPhone: phone,
PayType: "appAli",
ParamY: 26.996671,
ParamX: 77.450347,
Yanqian: true,
MobileOperatingPlatform: "iOS",
SysVersion: "iOS 15.7",
PlatformType: "iPad Pro (12.9-inch) (3rd generation)",
NetWork: "unknown",
Platform: "ios",
Brand: "apple",
DeviceId: strings.Replace(uuid.NewString(), "-", "", -1),
}
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: "",
BodyStr: bodyS,
Channel: "app",
Yanqian: true,
}
resp, err = c.Client.Post(ctx, "https://recharge3.bac365.com/camel_wechat_mini_oil_server/eCardMall/createShoppingOrder", req)
resp, err = c.Client.ContentJson().Post(ctx, "https://recharge3.bac365.com/camel_wechat_mini_oil_server/eCardMall/createShoppingOrder", req)
if err != nil {
return "", "", err
}
glog.Info(ctx, "登录", req, resp.ReadAllString())
respStr := resp.ReadAllString()
glog.Info(ctx, "登录", req, respStr)
respStruct := struct {
Code string `json:"code"`
Message string `json:"message"`
@@ -133,24 +185,103 @@ func (c *Client) CreateOrder(ctx context.Context, phone, token string, amount fl
} `json:"orderRes,omitempty"`
OrderId string `json:"orderid,omitempty"`
}{}
err = json.Unmarshal(resp.ReadAll(), &respStruct)
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")
}
return respStruct.OrderId, respStruct.OrderRes.Body, err
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 查询对应订单
func (c *Client) QueryOrder(ctx context.Context, phone, token, orderId string) (status bool, err error) {
//req := struct {
// OrderId string `json:"orderId"`
//}{
// OrderId: orderId,
//}
return false, nil
// 返回值说明:
// - 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
}

View File

@@ -0,0 +1,8 @@
package camel_oil_api
import (
"testing"
)
func TestClient_SendCaptcha(t *testing.T) {
}

View File

@@ -0,0 +1,207 @@
package camel_oil_api
import (
"bytes"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"errors"
"strings"
"github.com/duke-git/lancet/v2/convertor"
)
// EncryptWithPublicKey encrypts the input JSON string using RSA public key with block processing for large data.
// It uses PKCS1Padding and supports chunking based on key size minus 11 bytes for padding.
func EncryptWithPublicKey(base64PublicKey, jsonString string) (string, error) {
publicKey, err := parsePublicKey(base64PublicKey)
if err != nil {
return "", err
}
data := []byte(jsonString)
// Calculate maximum plaintext block size: modulus size - 11 bytes for PKCS1Padding
modulusSizeBytes := publicKey.N.BitLen() / 8
maxBlockSize := modulusSizeBytes - 11
var buf bytes.Buffer
offset := 0
for offset < len(data) {
end := min(offset+maxBlockSize, len(data))
encryptedBlock, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, data[offset:end])
if err != nil {
return "", err
}
// Each encrypted block is always the size of the modulus
buf.Write(encryptedBlock)
offset = end
}
return convertor.ToStdBase64(buf.Bytes()), nil
}
// DecryptWithPrivateKey decrypts the base64-encoded ciphertext using RSA private key with block processing for large data.
// It uses PKCS1Padding and follows the Java implementation: Math.min(blockSize, data.length - offset)
func DecryptWithPrivateKey(base64PrivateKey, ciphertext string) (string, error) {
privateKey, err := parsePrivateKey(base64PrivateKey)
if err != nil {
return "", err
}
// Decode base64-encoded ciphertext
cipherData, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
// RSA modulus size: each encrypted block is exactly this size
blockSize := privateKey.N.BitLen() / 8 // 1024-bit key = 128 bytes
var buf bytes.Buffer
offset := 0
for offset < len(cipherData) {
// Calculate current block size: min(blockSize, remaining data)
currentBlockSize := blockSize
if currentBlockSize > len(cipherData)-offset {
currentBlockSize = len(cipherData) - offset
}
decryptedBlock, err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, cipherData[offset:offset+currentBlockSize])
if err != nil {
return "", err
}
buf.Write(decryptedBlock)
offset += currentBlockSize
}
return buf.String(), nil
}
// DecryptWithPrivateThenEncryptWithPublic decrypts ciphertext with private key,
// then encrypts the result with public key. Commonly used for authorization token transformation.
func DecryptWithPrivateThenEncryptWithPublic(base64PrivateKey, base64PublicKey, ciphertext string) (string, error) {
privateKey, err := parsePrivateKey(base64PrivateKey)
if err != nil {
return "", err
}
publicKey, err := parsePublicKey(base64PublicKey)
if err != nil {
return "", err
}
// Decrypt ciphertext using private key
cipherData, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
plaintext, err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, cipherData)
if err != nil {
return "", err
}
// Encrypt plaintext using public key
encrypted, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, plaintext)
if err != nil {
return "", err
}
return convertor.ToStdBase64(encrypted), nil
}
// parsePrivateKey parses a private key from base64 or PEM format.
// Supports both base64-encoded PKCS#8 keys and PEM-wrapped keys.
func parsePrivateKey(keyStr string) (*rsa.PrivateKey, error) {
var keyBytes []byte
var err error
// Decode base64 if not PEM format
if strings.Contains(keyStr, "-----BEGIN") {
keyBytes = []byte(keyStr)
} else {
keyBytes, err = base64.StdEncoding.DecodeString(keyStr)
if err != nil {
return nil, err
}
}
// Extract key data from PEM block if present
if strings.Contains(keyStr, "-----BEGIN") {
block, _ := pem.Decode(keyBytes)
if block == nil {
return nil, errors.New("failed to parse PEM block")
}
keyBytes = block.Bytes
}
// Parse PKCS#8 private key
parsedKey, err := x509.ParsePKCS8PrivateKey(keyBytes)
if err != nil {
return nil, err
}
privateKey, ok := parsedKey.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("not an RSA private key")
}
return privateKey, nil
}
// parsePublicKey parses a public key from base64 or PEM format.
// Supports both base64-encoded X.509 keys and PEM-wrapped keys.
func parsePublicKey(keyStr string) (*rsa.PublicKey, error) {
var keyBytes []byte
var err error
// Decode base64 if not PEM format
if strings.Contains(keyStr, "-----BEGIN") {
keyBytes = []byte(keyStr)
} else {
keyBytes, err = base64.StdEncoding.DecodeString(keyStr)
if err != nil {
return nil, err
}
}
// Extract key data from PEM block if present
if strings.Contains(keyStr, "-----BEGIN") {
block, _ := pem.Decode(keyBytes)
if block == nil {
return nil, errors.New("failed to parse PEM block")
}
keyBytes = block.Bytes
}
// Parse X.509 public key
parsedKey, err := x509.ParsePKIXPublicKey(keyBytes)
if err != nil {
return nil, err
}
publicKey, ok := parsedKey.(*rsa.PublicKey)
if !ok {
return nil, errors.New("not an RSA public key")
}
return publicKey, nil
}
// Sign generates MD5 signature for the given openId.
// Signature = MD5("a488109389ef32d6ad546296b3260562" + openId)
func Sign(openId string) string {
data := "a488109389ef32d6ad546296b3260562" + openId
hash := md5.Sum([]byte(data))
return hex.EncodeToString(hash[:])
}

View File

@@ -0,0 +1,56 @@
package camel_oil_api
import "testing"
func TestDecryptWithPrivateThenEncryptWithPublic(t *testing.T) {
base64PublicKey := "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDpwypBPN8r3Zwv3T0XUh1Ka2m2hUe3KBgIyH4fHfN/T1jsBWnbwotKEQdZfRva7mRYiz9YrTHoH/eUAuv+WYqPMubaiqpWOu0l+BzEX1kPGA98qRC06IF2Tk4Z5xAmQ8p8u3O5jxohYFkO2XlDvPU+W9SDZgSEBTe8p80LExgo6wIDAQAB" // 公钥 base64 或带 PEM
base64PrivateKey := "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIHSkp+Z0Lu+lZWr/wKcMT3EcWEIihKTg/jEOyKaczqG9hWL9UULJ1dFtIQNlpWRySsVZcJLoGTFdGam557lVzpY/tbN73KG9iVMBaKALLF52cgmyg0DRve4atc0OnkhTjv7Rf8B85UokdHCAM/5MgNcjXwqBGHohJ2LGC9yN2erAgMBAAECgYATfTeqww4daTaOkhQF4cnYonl83inQMRoSSe8wuiwLQMCHqounEk4VIW9AlcOh75FaKOuuV+kbx7K6SFskNPy7nGYfS22t2aM9E9Rt+JH+caEniYi5qAfb3gCIgsGExUNI6iuSM2p3/R542EDGc2FyfPPqyht+jR4CjLOLoXHfoQJBALwvF6uIOSW0Lxh7Lo/JsKpWJ1qffDvXWYag605L9JAyP0yO64woF60Tn+mGRzcaEhNDSEjinKQqPEJnxDUGYaECQQCwm1mQKD95MaeKWBiOVJZsdzL5aJsW42xyiu0ZwA7bZUgJyUskzXG0ubeIHK/czlJbev9ODubbMNJFcngX4N3LAkAJaxH0M80oZew1fXTHHYEKBWXS00iUdiK06jjcolCLJvikDEMdsKP+tYy7U00dJODitetYOn88eCCr8iWPwdIBAkBtUjzGt6NS6iHDyXSp5kKXMdIkAVS/flgLL2RFpFWOCcvmAuy5A1N3g97QKrHSBQWGC0UulJri4/3Fb25XmaKxAkBifs9dbUifeqZRNVh2Omck4xedb1FyQPLDicUycjYug3Vca0T/LRr80aX/NhbhtpSdwzF1ukiZ6W46O9DmGuNy" // 私钥 base64 或带 PEM
token := "M20UkqDpKp7u3wO1wxSGlBvFWS4AAVjcglRw5Feh6ezvxW4NtoX0V8dGajhCK1+9Gf+Yqb+dzrQ0evYKe7kpqyeXtqg5xobQ6Sx1MQsf6eV9kUBU1u/qS71wlIgDYJhtMk81qfF+Ojx1FCFzV+89jKLr3jeXf2QeRv/ZqMXIqb0=" // 待私钥解密的密文 base64
result, _ := DecryptWithPrivateThenEncryptWithPublic(base64PrivateKey, base64PublicKey, token)
t.Log(result)
}
func TestEnc(t *testing.T) {
pubkey := "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCkc6Xr/JhWEx/WPxG2q3VHLQ+FYk/oCmQ1y14B5j4xOJY+mAWoDOOti3sAXg0Kk662gWjWET1nLI6YED4wb9HWon1NAZn47lgc5ohIpEdU91Jao85X/kgkD3NvTTvhFicttepUOsrYUZN8rAQCE7AhzwGgKnCiIRY/kE8jOCCeZQIDAQAB"
jsonString := "{\"openId\":\"app2511152349246268055\",\"phone\":\"13349946900\",\"goodId\":\"202511051701\",\"goodNum\":1,\"bindPhone\":\"13349946900\",\"payType\":\"appAli\",\"paramY\":26.996671,\"paramX\":77.450347,\"yanqian\":true,\"mobileOperatingPlatform\":\"iOS\",\"sysVersion\":\"iOS 15.7\",\"platformType\":\"iPhone X\",\"netWork\":\"wifi\",\"platform\":\"iOS\",\"brand\":\"Apple\",\"deviceId\":\"A6F2C1C0-8E65-4E4A-9A1B-3D8C9879A41E\"}"
result, _ := EncryptWithPublicKey(pubkey, jsonString)
t.Log(result)
}
func TestSign(t *testing.T) {
// 测试 Sign 函数
openId := "app2511181557205741495"
result := Sign(openId)
t.Logf("Sign result: %s", result)
// Java 中的签名结果(根据 demo.java
// 不需要匹配具体值,简单验证应输出 MD5 值
if result == "" {
t.Fatalf("Sign result is empty")
}
t.Logf("Test passed! OpenId: %s, Signature: %s", openId, result)
}
func TestDec(t *testing.T) {
// 使用 Java demo.java 中的源数据进行测试
// 这是用来加密的公钥
prikey := "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=="
//encryptedBody := "YhOjfuxANpCb0a4/ohwCWA5RFnj9EqY0r75nGD0IUJRrt0W4t1mOg0BcpHMwq0y3V+83ZmiLOh8+ckAzvQ4U6dQxYekcSmD9rOTGUE3mDWBpsOCP4gIGs5qitJKekvaQ/ILymtGvunhEOS3iEOfFZUptwi6zPOIzEU7kRlCNKXeSlj603PZo2vqwy3tn4osNOFPsZP+09FeudAbz9Ad0Niq8jfYbdXmopauoj2+nQFfBdw486wN4buGfJ5ZC1tWwWzzj1X0Mcibu68vp/uH5xYLn0/zJm/bDlxnTbcCA1aFd6bp/wom+KmvdhJfpS5bDlfddHg/5NefufG6uc2mWaDo41phUx9zgpJZkBjK5OEA1JeRz8KCHUiEET6pZsf65zFgGTE0qbieiHOkfYY4jb88AvzJgN5LgBYiG2GoCIzE0Zt8XlUfNwlZST9TW0FaYd0ftvtebKJbZ6ilJPiRgAW9Mh4i6RakbrVBSP/Y/CpL1uzpoQkiIdQ0YV0ED0rFSI4BsOExYl5dSsUfk1JJgFD2+NaU6uqyrxNMp8bySly1n0jU7TaHyxQVxb5tX/WOn78hwc0Qu7tMAnn7PcLWvf7FRG9ILGRYoS1odVhg4BvqSfHL/abZWriWP3bY+gNgK5U6GbtQr8wTGm0eyse5lrWTw1+OezT+jxOyCdJN6Z/uFTRvW5m9Hv1xxDg2qUOW4YKqU0fbb0vmOiF/2KpxvA3y8FLJ2jx3lDFaxzf8w9djwA8YkoiLWlE+/b5iQtOVqHeiYxySOvF+NTIh3AldbVGIUMc8BeCNMjM32Lbz9dkKl+gJeh/cKKZYP8n58ux48KxjR4LkDJAkpUJTQLy389mdizwGxswa8DkhUtAsGNw4apgh58wAUt/AVj92XiH+BCGrcqxfkiw792ycGs7DHAEuuWBnvROteGhFQ98d1nGoktrbh/p0M6jmygktNWcD4gNeXgZGvV+2BUdZSWNNdH2iDImscuYXOHtti6cDBZcULhWeiOVIIBMK1hIBevKsfoYbnhe8U32w//vJUotUlLywOiC0O1uVS5moS4/jWRo0XFCd6lUyxZ1sq1bKO9p9K3lxVlswMoDyLIAc1/xmDnJs6umzGuMPytdOgoHXloI2dnMuJw2aEZOfzTAte6aicsUbaqdJ6Mzhr869b0l4lhMyUpNFWLpccNVkKLP9BpSRDheWw1shKf1fX6eQLLOq6m7n5bSMAZ1psYDTUbq3SZCVx2j47MxHRl8SD4iGirFmye2k04I3d0Ldwtzq36yoNKmsRh7NtxBc2iKT2XmcRxYheh1cx+tnccmnII7ajIEKTwFBO4GRxfT1fKDt4mWvbFaXu+ZYNPIdgw505zutEVmJVaY+JGtTI0euGDEzPfhWGhbxmlGc/u5QpajatUE7ZiXLuQiBbNCpvIvf6ZsAzUPsV7Mzw7qfUMtSol7qW1cghgMwQvwSsr4F0FRFnJDY9Eox76LLKKwuTUJFKUuRadjRqyV2HfMSp4EE54LJS0zNmdeIxfiimIbz2D5Y/7S+lQ72a/wyKI1MtY1jCnK7IP+No3Vp0p1MmnXjVtmuR2zsniUSDJRnc+Pyb6+PaBtM0pTFzp6ghKlEeKYkKB/Jt2vDqqNO2/C20e1Fl93f0bf5mL3H6ZQU4ok8sUMWYnRHHK83fNP5MPwlD6a0ZsWEAdjLmsR1LjzB2qYzXTUxKf8d0fLg7txW6VdajDVsCSzhpkdxGjRhGZsiMpPuJDG1nJgQO140csQ2kch5cSA4DyCTkzV02Xe6ztLkXS2MnDLHHwbmB7qrYsyLQcsl5ldOqs7i4A38UgikEli/fm7kVrhLQMjJUlyhvtESTKkf4tm4gLacOSIl1BdS5YfkgFIpaEYglW+C7ChOk/YFq+ZYKR9akRK+xnDUrG+p0KhiQ5bdiYxGWEO/RbrHA9pa0iDyQQ103HNRNfmSFLWgrzVTT+gxjw3hpe7Pd4MenVYCZZSxNTR+RqC8/rUW5+nINr9F2HDTDrvg5ZDDND4itVCp7gXoiN1E/0m3V16k5Zcw9e+vJeb3Sld2g1/VRULDAgXCuhC4XIak1oH6jelrJMVJzCizYUGUgZbbGrByw1EsWddE1xBpK+5vGkvsjkjflXN1YQsIzZskcw7qHETilmp1U3kCvBlOPirU84lmR/5j9sjgkxfsrSpKJoxGFQ1OxuOwA2pvbbnc7ENK6gbTeV91fQ/JZ8FeycgdXAuFefi68WmGcl64h+jpfcmrlKJjaCei6VjWj3xiuTHtnv7SlXNaRPzZ1Nv183c8IpUtd5jWEaufpSTe+RYLFvZa2K0FRyYoPtf0gtIJOqpdxAXCT8m7jjxTU9bNWVrHAdsCxpeNGNRQerhp82zFL0UksbekzoYj5k06R6i+RNeE0jJLYBd1jeRWvj622m5Mo+Mqe0hvLq4WnwJa0ZIhxWR15eaE1D3Ad94sbDS9lUDTH/BLVXOXI5EODwmOcnvahxWl1VIokYqQHDsqHKidzSO63MMSvySIwFWLpKXe5oCuqbYyMs+oJop20NdCTjsW0cnQqDGakwyyXTMqgOoWkweWXqduE6ZmnNvRZKxZTgvpte9LcG4hRB9sLFt2/544uwSqQNO4GYHOFjMG0xRStBNTBPie4c5GHyuBAXMboKpt5466PIaTVW/AnCLRk3uDZHq6r3mYJkBv8peqQpW/LcZPHZVakiiLSQrxNOTi532jxmH5COu4FsCk4mDCK5fYS4N/rnZkBTgfHJY3StmhR5xmj0FrUthGe2qDTWT/MSTs3U2+Ryj3dT02pQHISoIGFQ3Mh612G9Q5VAt6CAqn3HrX/TfIODCnMP9wpBwA08/pvnGgOykVvVdGwXX/oDk8pVaTJAYPymTW0h5qug92Ms0I0LR0JxaMPbQ=="
encryptedBody := "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"
// Use DecryptWithPrivateKey function to decrypt
decrypted, err := DecryptWithPrivateKey(prikey, encryptedBody)
if err != nil {
t.Fatalf("Dec failed: %v", err)
}
t.Logf("Decrypted: %s", decrypted)
t.Log("Test passed! Decryption matches Java result.")
// const orderStr = 'alipay_sdk=xxx...';
//const encodedPayUrl = encodeURIComponent(orderStr);
//alipay: //platformapi/startapp?appId=20000067&url=alipay-sdk-java-4.40.33.ALL%26app_id%3d2021005196642063%26biz_content%3d%257B%2522business_params%2522%253A%2522%257B%255C%2522outTradeRiskInfo%255C%2522%253A%255C%2522%257B%255C%255C%255C%2522sysVersion%255C%255C%255C%2522%253A%255C%255C%255C%2522iOS%2b26.2%255C%255C%255C%2522%252C%255C%255C%255C%2522mcCreateTradePackage%255C%255C%255C%2522%253A%255C%255C%255C%2522com.ltjy.uni1005507%255C%255C%255C%2522%252C%255C%255C%255C%2522mcCreateTradeTime%255C%255C%255C%2522%253A%255C%255C%255C%25222025-11-22%2b01%253A39%253A56%255C%255C%255C%2522%252C%255C%255C%255C%2522mcCreateTradeLbs%255C%255C%255C%2522%253A%255C%255C%255C%2522118.9885703919527%252C36.36133429662592%255C%255C%255C%2522%252C%255C%255C%255C%2522mobileOperatingPlatform%255C%255C%255C%2522%253A%255C%255C%255C%2522ios%255C%255C%255C%2522%252C%255C%255C%255C%2522platformType%255C%255C%255C%2522%253A%255C%255C%255C%2522iPad%2bPro%2b%252812.9-inch%2529%2b%25283rd%2bgeneration%2529%255C%255C%255C%2522%252C%255C%255C%255C%2522mcCreateTradeChannel%255C%255C%255C%2522%253A%255C%255C%255C%2522app%255C%255C%255C%2522%252C%255C%255C%255C%2522extraAccountRegTime%255C%255C%255C%2522%253A%255C%255C%255C%25222025-11-18%2b15%253A57%253A21%255C%255C%255C%2522%252C%255C%255C%255C%2522extraAccountPhone%255C%255C%255C%2522%253A%255C%255C%255C%252217862666120%255C%255C%255C%2522%252C%255C%255C%255C%2522netWork%255C%255C%255C%2522%253A%255C%255C%255C%2522unknown%255C%255C%255C%2522%257D%255C%2522%252C%255C%2522mc_create_trade_ip%255C%2522%253A%255C%2522123.168.253.227%255C%2522%257D%2522%252C%2522out_trade_no%2522%253A%25222511220139557299472%2522%252C%2522subject%2522%253A%2522%25E9%25AA%2586%25E9%25A9%25BC%25E6%258A%25B5%25E6%2589%25A3%25E5%2588%25B8-50%25E5%2585%2583%25E9%259D%25A2%25E5%2580%25BC%2522%252C%2522timeout_express%2522%253A%252210m%2522%252C%2522total_amount%2522%253A%252250.00%2522%257D%26charset%3dUTF-8%26format%3djson%26method%3dalipay.trade.app.pay%26notify_url%3dhttps%253A%252F%252Frecharge3.bac365.com%252Fpayment%252Falipay%252Fnotify%26sign%3dDQ0Kf5KCZ1jUHptXuAriStVEHDbKdeNWofxZr%252FAL%252BAA%252FYytu0kXNTDkviGLKMG9je2wS419dKGqX8inT9y7G%252FtcactTKoLavi3gy6RLys0Yof97B1umkopzqIhQ2nA9lxDsaLEfqXfdv33zaRetQFgPGzIv5KrOPRY18RWvwp5PqKodSp9AzbJ5bAGELTzbJ8Oj4YOeNSFelsOUaD0J4Ecw%252B7qmDIDb8UDIBYXFaNUquPtWE%252FjAZ%252FGM9PWtMYx7%252Fiq2mmbJJTe5ErN7L%252BtsXGV51axZ2f%252F0EZKISMKkgI0I5ECnHZjGa2pw%252FByMeMREYF05ZuF%252Fowcn53Snag3j0aw%253D%253D%26sign_type%3dRSA2%26timestamp%3d2025-11-22%2b01%253A39%253A56%26version%3d1.0
//window.location.href = alipayScheme;
}

View File

@@ -0,0 +1,7 @@
package camel_oil_api
type QueryResult struct {
CardNumber string `json:"card_number"`
CardPassword string `json:"card_password"`
Balance float64 `json:"balance"`
}

View File

@@ -63,27 +63,35 @@ func (c *InternalClient) getToken(ctx context.Context) (string, error) {
// GetAccountInfo 获取账号信息
func (c *InternalClient) GetAccountInfo(ctx context.Context) (string, error) {
token, err := c.getToken(ctx)
if err != nil {
return "", err
}
respBody := struct {
Token string `json:"token"`
SID int `json:"sid"`
}{
Token: token,
SID: 21108,
}
//尝试100次直到获取到号码为止
for range 100 {
token, err := c.getToken(ctx)
if err != nil {
return "", err
}
respBody := struct {
Token string `json:"token"`
SID int `json:"sid"`
}{
Token: token,
SID: 21108,
}
resp, err := c.Client.Post(ctx, fmt.Sprintf("https://api.haozhuyun.com/sms?api=getPhone&token=%s&sid=%d&Province=&ascription=&isp=", respBody.Token, respBody.SID))
if err != nil {
return "", err
resp, err := c.Client.Post(ctx, fmt.Sprintf("https://api.haozhuyun.com/sms?api=getPhone&token=%s&sid=%d&Province=&ascription=&isp=", respBody.Token, respBody.SID))
if err != nil {
return "", err
}
respStr := resp.ReadAllString()
glog.Info(ctx, "获取信息", respStr)
respStruct := struct {
Phone string `json:"phone"`
}{}
err = json.Unmarshal([]byte(respStr), &respStruct)
if respStruct.Phone != "" {
return respStruct.Phone, nil
}
}
respStruct := struct {
Phone string `json:"phone"`
}{}
err = json.Unmarshal(resp.ReadAll(), &respStruct)
return respStruct.Phone, err
return "", fmt.Errorf("获取账号失败")
}
// CheckVerifyCode 检测验证码是否已接收
@@ -97,13 +105,15 @@ func (c *InternalClient) CheckVerifyCode(ctx context.Context, phone string) (cod
if err != nil {
return "", false, err
}
respStr := resp.ReadAllString()
glog.Info(ctx, "获取信息", respStr)
respStruct := struct {
Code json.Number `json:"code"`
Msg string `json:"msg"`
Sms string `json:"sms"`
Yzm string `json:"yzm"`
}{}
err = json.Unmarshal(resp.ReadAll(), &respStruct)
err = json.Unmarshal([]byte(respStr), &respStruct)
if err != nil {
return "", false, err
}

View File

@@ -5,15 +5,11 @@ import (
_ "github.com/gogf/gf/contrib/nosql/redis/v2"
"github.com/gogf/gf/v2/os/glog"
"testing"
"time"
)
func TestInternalClient_GetAccountInfo(t *testing.T) {
account, _ := NewClient().GetAccountInfo(t.Context())
count := 10 * 100
glog.Info(t.Context(), "账号信息:", account)
for i := 0; i < count; i++ {
time.Sleep(time.Second * 5)
NewClient().CheckVerifyCode(t.Context(), account)
for i := 0; i < 10; i++ {
account, _ := NewClient().GetAccountInfo(t.Context())
glog.Info(t.Context(), account)
}
}