feat(camel_oil): 新增Token自动登录与验证码管理功能

- 新增Token状态:待验证码、登录失败、验证码已发送等多种状态
- 修改Token模型及数据库结构,新增登录token及过期时间字段
- 创建Token时,默认状态为待验证码,异步发送验证码
- 实现Token登录接口,支持验证码登录并更新登录token信息
- 支持重发验证码接口及获取需登录Token列表接口
- 添加Token自动登录定时任务,自动发送验证码和重试登录
- 优化账号列表查询,默认按状态升序排列,搜索支持模糊匹配
- 调整API和服务接口,统一Token名称和状态相关字段命名
- 修正绑定卡密接口使用登录token替代旧token值登录
- 新增Token管理相关单元测试用例,覆盖新增功能逻辑
This commit is contained in:
danial
2025-12-08 23:02:24 +08:00
parent 1e2b734b19
commit 3ef482357b
15 changed files with 435 additions and 89 deletions

View File

@@ -2,11 +2,10 @@ package v1
import (
"encoding/json"
"kami/api/commonApi"
"kami/utility/utils"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"kami/api/commonApi"
"kami/internal/consts"
)
// ====================================================================================
@@ -16,9 +15,8 @@ import (
// CreateTokenReq 创建 Token 请求
type CreateTokenReq struct {
g.Meta `path:"/token/create" tags:"JD V2 Token Management" method:"post" summary:"创建 Token"`
TokenName string `json:"tokenName" v:"required" description:"Token名称"`
TokenValue string `json:"tokenValue" v:"required" description:"Token值"`
Phone string `json:"phone" description:"绑定的手机号"`
Name string `json:"name" v:"required" description:"名称"`
Phone string `json:"phone" v:"required" description:"绑定的手机号"`
Remark string `json:"remark" description:"备注"`
RechargeLimitAmount float64 `json:"rechargeLimitAmount" v:"required" description:"充值金额限制"`
RechargeLimitCount int `json:"rechargeLimitCount" v:"required" description:"充值次数限制"`
@@ -37,22 +35,22 @@ type GetTokenReq struct {
// TokenInfo Token 信息
type TokenInfo struct {
Id int64 `json:"id" description:"Token ID"`
UserId string `json:"userId" description:"用户ID空字符串表示管理员创建"`
TokenName string `json:"tokenName" description:"Token名称"`
TokenValue string `json:"tokenValue" description:"Token值"`
Phone string `json:"phone" description:"绑定的手机号"`
Status int `json:"status" description:"状态"`
BindCount int `json:"bindCount" description:"已绑定卡密数量"`
TotalBindAmount float64 `json:"totalBindAmount" description:"累计绑定金额"`
TotalRechargeAmount float64 `json:"totalRechargeAmount" description:"充值金额"`
RechargeLimitAmount float64 `json:"rechargeLimitAmount" description:"充值金额限制"`
RechargeLimitCount int `json:"rechargeLimitCount" description:"充值次数限制"`
LastBindAt *gtime.Time `json:"lastBindAt" description:"最后绑定时间"`
LastUsedAt *gtime.Time `json:"lastUsedAt" description:"最后使用时间"`
Remark string `json:"remark" description:"备注"`
CreatedAt *gtime.Time `json:"createdAt" description:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" description:"更新时间"`
Id int64 `json:"id" description:"Token ID"`
UserId string `json:"userId" description:"用户ID空字符串表示管理员创建"`
Name string `json:"name" description:"名称"`
Phone string `json:"phone" description:"绑定的手机号"`
Status consts.CamelOilTokenStatus `json:"status" description:"状态"`
BindCount int `json:"bindCount" description:"已绑定卡密数量"`
TotalBindAmount float64 `json:"totalBindAmount" description:"累计绑定金额"`
TotalRechargeAmount float64 `json:"totalRechargeAmount" description:"总充值金额"`
RechargeLimitAmount float64 `json:"rechargeLimitAmount" description:"充值金额限制"`
RechargeLimitCount int `json:"rechargeLimitCount" description:"充值次数限制"`
LastBindAt *gtime.Time `json:"lastBindAt" description:"最后绑定时间"`
LastUsedAt *gtime.Time `json:"lastUsedAt" description:"最后使用时间"`
LastLoginAt *gtime.Time `json:"lastLoginAt" description:"最后登录时间"`
Remark string `json:"remark" description:"备注"`
CreatedAt *gtime.Time `json:"createdAt" description:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" description:"更新时间"`
}
// GetTokenRes 获取 Token 信息响应
@@ -69,15 +67,15 @@ type ListTokensRes struct {
type ListTokensReq struct {
g.Meta `path:"/token/list" tags:"JD V2 Token Management" method:"get" summary:"列出 Token"`
commonApi.CommonPageReq
TokenName string `json:"tokenName" description:"Token名称"`
Status int `json:"status" description:"状态"`
Name string `json:"name" description:"名称"`
Status consts.CamelOilTokenStatus `json:"status" description:"状态"`
}
// UpdateTokenReq 修改 Token 请求
type UpdateTokenReq struct {
g.Meta `path:"/token/update" tags:"JD V2 Token Management" method:"post" summary:"修改 Token"`
TokenId int64 `json:"tokenId" v:"required" description:"Token ID"`
TokenName string `json:"tokenName" v:"required" description:"Token名称"`
Name string `json:"name" v:"required" description:"名称"`
Phone string `json:"phone" description:"绑定的手机号"`
Remark string `json:"remark" description:"备注"`
RechargeLimitAmount float64 `json:"rechargeLimitAmount" v:"required" description:"充值金额限制"`
@@ -108,7 +106,7 @@ type DeleteTokenRes struct {
type CardBindingInfo struct {
Id int64 `json:"id" description:"绑定记录ID"`
TokenId int64 `json:"tokenId" description:"Token ID"`
TokenName string `json:"tokenName" description:"Token名称"`
Name string `json:"name" description:"Token名称"`
OrderId int64 `json:"orderId" description:"订单ID"`
CardNumber string `json:"cardNumber" description:"卡号"`
CardPassword string `json:"cardPassword" description:"卡密"`
@@ -134,8 +132,7 @@ func (t TokenInfo) MarshalJSON() ([]byte, error) {
return json.Marshal(Alias{
Id: t.Id,
UserId: t.UserId,
TokenName: t.TokenName,
TokenValue: utils.MaskTokenValue(t.TokenValue),
Name: t.Name,
Phone: t.Phone,
Status: t.Status,
BindCount: t.BindCount,
@@ -145,6 +142,7 @@ func (t TokenInfo) MarshalJSON() ([]byte, error) {
RechargeLimitCount: t.RechargeLimitCount,
LastBindAt: t.LastBindAt,
LastUsedAt: t.LastUsedAt,
LastLoginAt: t.LastLoginAt,
Remark: t.Remark,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,

View File

@@ -84,16 +84,22 @@ var CamelOilCallbackStatusText = map[CamelOilNotifyStatus]string{
type CamelOilTokenStatus int
const (
CamelOilTokenStatusAvailable CamelOilTokenStatus = 1 // 可用
CamelOilTokenStatusDisabled CamelOilTokenStatus = 2 // 已
CamelOilTokenStatusExpired CamelOilTokenStatus = 3 // 已过期
CamelOilTokenStatusPendingVerification CamelOilTokenStatus = 0 // 待验证码
CamelOilTokenStatusAvailable CamelOilTokenStatus = 1 // 已登录/可
CamelOilTokenStatusDisabled CamelOilTokenStatus = 2 // 已禁用
CamelOilTokenStatusExpired CamelOilTokenStatus = 3 // 已过期
CamelOilTokenStatusLoginFailed CamelOilTokenStatus = 4 // 登录失败
CamelOilTokenStatusCodeSent CamelOilTokenStatus = 5 // 验证码已发送待登录
)
// CamelOilTokenStatusText Token状态文本映射
var CamelOilTokenStatusText = map[CamelOilTokenStatus]string{
CamelOilTokenStatusAvailable: "可用",
CamelOilTokenStatusDisabled: "已禁用",
CamelOilTokenStatusExpired: "已过期",
CamelOilTokenStatusPendingVerification: "待验证码",
CamelOilTokenStatusAvailable: "已登录",
CamelOilTokenStatusDisabled: "已禁用",
CamelOilTokenStatusExpired: "已过期",
CamelOilTokenStatusLoginFailed: "登录失败",
CamelOilTokenStatusCodeSent: "验证码已发送",
}
// ====================================================================================

View File

@@ -6,6 +6,7 @@ import (
"github.com/gogf/gf/v2/errors/gcode"
v1 "kami/api/camel_oil/v1"
"kami/internal/errHandler"
"kami/internal/model"
"kami/internal/service"
)
@@ -15,7 +16,13 @@ func (c *ControllerV1) CreateToken(ctx context.Context, req *v1.CreateTokenReq)
err = errHandler.WrapError(ctx, gcode.CodeInternalError, err, "登录校验失败")
return
}
tokenId, err := service.CamelOil().CreateToken(ctx, req.TokenName, req.TokenValue, req.Phone, req.Remark, req.RechargeLimitAmount, req.RechargeLimitCount)
tokenId, err := service.CamelOil().CreateToken(ctx, &model.CamelOilTokenCreateInput{
Name: req.Name,
Phone: req.Phone,
Remark: req.Remark,
RechargeLimitAmount: req.RechargeLimitAmount,
RechargeLimitCount: req.RechargeLimitCount,
})
if err != nil {
return nil, err
}

View File

@@ -5,9 +5,9 @@ import (
"github.com/gogf/gf/v2/errors/gcode"
v1 "kami/api/camel_oil/v1"
"kami/internal/consts"
"kami/internal/errHandler"
"kami/internal/service"
"kami/utility/utils"
)
func (c *ControllerV1) GetToken(ctx context.Context, req *v1.GetTokenReq) (res *v1.GetTokenRes, err error) {
@@ -24,10 +24,9 @@ func (c *ControllerV1) GetToken(ctx context.Context, req *v1.GetTokenReq) (res *
Token: v1.TokenInfo{
Id: token.Id,
UserId: token.UserId,
TokenName: token.TokenName,
TokenValue: utils.MaskTokenValue(token.TokenValue),
Name: token.Name,
Phone: token.Phone,
Status: token.Status,
Status: consts.CamelOilTokenStatus(token.Status),
BindCount: token.BindCount,
TotalBindAmount: token.TotalBindAmount.InexactFloat64(),
TotalRechargeAmount: token.TotalRechargeAmount.InexactFloat64(),
@@ -35,6 +34,7 @@ func (c *ControllerV1) GetToken(ctx context.Context, req *v1.GetTokenReq) (res *
RechargeLimitCount: token.RechargeLimitCount,
LastBindAt: token.LastBindAt,
LastUsedAt: token.LastUsedAt,
LastLoginAt: token.LastLoginAt,
Remark: token.Remark,
CreatedAt: token.CreatedAt,
UpdatedAt: token.UpdatedAt,

View File

@@ -6,9 +6,9 @@ import (
"github.com/gogf/gf/v2/errors/gcode"
v1 "kami/api/camel_oil/v1"
"kami/api/commonApi"
"kami/internal/consts"
"kami/internal/errHandler"
"kami/internal/service"
"kami/utility/utils"
)
func (c *ControllerV1) ListTokens(ctx context.Context, req *v1.ListTokensReq) (res *v1.ListTokensRes, err error) {
@@ -21,7 +21,7 @@ func (c *ControllerV1) ListTokens(ctx context.Context, req *v1.ListTokensReq) (r
// 调用服务层方法,它会根据当前用户类型自动筛选数据
// 管理员可以查看所有token包括用户创建和管理员创建的
// 普通用户只能查看自己创建的token
tokens, total, err := service.CamelOil().ListTokensWithPagination(ctx, req.CommonPageReq, req.TokenName, req.Status)
tokens, total, err := service.CamelOil().ListTokensWithPagination(ctx, req.CommonPageReq, req.Name, int(req.Status))
if err != nil {
return nil, err
}
@@ -30,10 +30,9 @@ func (c *ControllerV1) ListTokens(ctx context.Context, req *v1.ListTokensReq) (r
tokenInfos = append(tokenInfos, v1.TokenInfo{
Id: token.Id,
UserId: token.UserId,
TokenName: token.TokenName,
TokenValue: utils.MaskTokenValue(token.TokenValue),
Name: token.Name,
Phone: token.Phone,
Status: token.Status,
Status: consts.CamelOilTokenStatus(token.Status),
BindCount: token.BindCount,
TotalBindAmount: token.TotalBindAmount.InexactFloat64(),
TotalRechargeAmount: token.TotalRechargeAmount.InexactFloat64(),
@@ -41,6 +40,7 @@ func (c *ControllerV1) ListTokens(ctx context.Context, req *v1.ListTokensReq) (r
RechargeLimitCount: token.RechargeLimitCount,
LastBindAt: token.LastBindAt,
LastUsedAt: token.LastUsedAt,
LastLoginAt: token.LastLoginAt,
Remark: token.Remark,
CreatedAt: token.CreatedAt,
UpdatedAt: token.UpdatedAt,

View File

@@ -15,7 +15,7 @@ func (c *ControllerV1) UpdateToken(ctx context.Context, req *v1.UpdateTokenReq)
err = errHandler.WrapError(ctx, gcode.CodeInternalError, err, "登录校验失败")
return
}
err = service.CamelOil().UpdateTokenInfo(ctx, req.TokenId, req.TokenName, req.Phone, req.Remark, req.RechargeLimitAmount, req.RechargeLimitCount)
err = service.CamelOil().UpdateTokenInfo(ctx, req.TokenId, req.Name, req.Phone, req.Remark, req.RechargeLimitAmount, req.RechargeLimitCount)
if err != nil {
return nil, err
}

View File

@@ -23,10 +23,12 @@ type V1CamelOilTokenDao struct {
type V1CamelOilTokenColumns struct {
Id string // 主键ID
UserId string // 用户ID空字符串表示管理员创建
TokenName string // Token名称/标识
TokenValue string // Token具体值
Name string // 名称
Phone string // 绑定的手机号
Status string // 状态1可用 2已禁用 3已过期
Status string // 状态:0待验证码 1可用 2已禁用 3已过期 4登录失败
LoginToken string // 登录后获取的token
LoginTokenExpiresAt string // 登录token过期时间
LastLoginAt string // 最后登录时间
BindCount string // 已绑定卡密数量
TotalBindAmount string // 累计绑定金额
LastBindAt string // 最后绑定时间
@@ -44,10 +46,12 @@ type V1CamelOilTokenColumns struct {
var v1CamelOilTokenColumns = V1CamelOilTokenColumns{
Id: "id",
UserId: "user_id",
TokenName: "token_name",
TokenValue: "token_value",
Name: "name",
Phone: "phone",
Status: "status",
LoginToken: "login_token",
LoginTokenExpiresAt: "login_token_expires_at",
LastLoginAt: "last_login_at",
BindCount: "bind_count",
TotalBindAmount: "total_bind_amount",
LastBindAt: "last_bind_at",

View File

@@ -11,6 +11,7 @@ import (
"kami/internal/model/do"
"kami/internal/model/entity"
"kami/utility/config"
"kami/utility/utils"
)
// ====================================================================================
@@ -137,8 +138,7 @@ func (s *sCamelOil) ListAccounts(ctx context.Context, status int, current, pageS
// ListAccount 查询账号列表API版本
func (s *sCamelOil) ListAccount(ctx context.Context, req *v1.ListAccountReq) (res *v1.ListAccountRes, err error) {
m := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1())
m := dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1()).OrderAsc(dao.V1CamelOilAccount.Columns().Status)
// 构建查询条件
if req.Status > 0 {
m = m.Where(dao.V1CamelOilAccount.Columns().Status, int(req.Status))
@@ -146,7 +146,7 @@ func (s *sCamelOil) ListAccount(ctx context.Context, req *v1.ListAccountReq) (re
if req.Keyword != "" {
// 基于账号ID、账号名称或手机号搜索
m = m.Where("(id LIKE ? OR account_name LIKE ? OR phone LIKE ?)",
"%"+req.Keyword+"%", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
utils.OrmLike(req.Keyword), utils.OrmLike(req.Keyword), utils.OrmLike(req.Keyword))
}
// 查询总数
@@ -157,7 +157,7 @@ func (s *sCamelOil) ListAccount(ctx context.Context, req *v1.ListAccountReq) (re
// 分页查询
var accounts []*entity.V1CamelOilAccount
err = m.Page(req.Current, req.PageSize).OrderDesc(dao.V1CamelOilAccount.Columns().CreatedAt).Scan(&accounts)
err = m.Page(req.Current, req.PageSize).Scan(&accounts)
if err != nil {
return nil, gerror.Wrap(err, "查询账号列表失败")
}

View File

@@ -0,0 +1,10 @@
package camel_oil
import (
v1 "kami/api/camel_oil/v1"
"testing"
)
func Test_sCamelOil_ListAccount(t *testing.T) {
(&sCamelOil{}).ListAccount(t.Context(), &v1.ListAccountReq{Keyword: "12345", Status: 2})
}

View File

@@ -405,3 +405,157 @@ func (s *sCamelOil) CronCleanExpiredPrefetchOrders(ctx context.Context) (cleaned
glog.Infof(ctx, "清理过期预拉取订单完成,清理数量: %d", cleanedCount)
return cleanedCount, nil
}
// CronTokenLoginTask Token 自动登录定时任务 - 由cron调度器定期调用
// 流程:检查需要发送验证码或重新登录的 Token自动处理登录流程
func (s *sCamelOil) CronTokenLoginTask(ctx context.Context) error {
glog.Info(ctx, "开始执行 Token 自动登录任务")
// 1. 处理需要发送验证码的 Token登录状态为 3验证码待获取
needCodeTokens, err := s.GetTokensNeedingCode(ctx)
if err != nil {
glog.Errorf(ctx, "查询需要验证码的 Token 失败: %v", err)
return err
}
if len(needCodeTokens) > 0 {
glog.Infof(ctx, "查询到 %d 个需要验证码的 Token", len(needCodeTokens))
codeSuccessCount := 0
codeFailCount := 0
for _, token := range needCodeTokens {
// 检查是否需要重试(避免频繁请求)
if token.UpdatedAt != nil && gtime.Now().Sub(token.UpdatedAt) < time.Minute*5 {
continue
}
err := s.SendVerificationCode(ctx, token.Id, token.Phone)
if err != nil {
glog.Errorf(ctx, "发送验证码失败Token ID: %d, 错误: %v", token.Id, err)
codeFailCount++
} else {
codeSuccessCount++
}
}
glog.Infof(ctx, "验证码发送完成: 成功=%d, 失败=%d", codeSuccessCount, codeFailCount)
}
// 2. 处理登录失败的 Token登录状态为 2登录失败
failedTokens, err := s.GetFailedLoginTokens(ctx)
if err != nil {
glog.Errorf(ctx, "查询登录失败的 Token 失败: %v", err)
return err
}
if len(failedTokens) > 0 {
glog.Infof(ctx, "查询到 %d 个登录失败的 Token", len(failedTokens))
retrySuccessCount := 0
retryFailCount := 0
for _, token := range failedTokens {
// 检查是否需要重试避免频繁重试间隔至少30分钟
if token.UpdatedAt != nil && gtime.Now().Sub(token.UpdatedAt) < time.Minute*30 {
continue
}
// 重置状态并发送验证码
err := s.SendVerificationCode(ctx, token.Id, token.Phone)
if err != nil {
glog.Errorf(ctx, "重试发送验证码失败Token ID: %d, 错误: %v", token.Id, err)
retryFailCount++
} else {
retrySuccessCount++
}
}
glog.Infof(ctx, "登录失败 Token 重试完成: 成功=%d, 失败=%d", retrySuccessCount, retryFailCount)
}
// 3. 清理过期的 Token登录 token 过期的)
expiredCount, err := s.CleanExpiredLoginTokens(ctx)
if err != nil {
glog.Errorf(ctx, "清理过期登录 Token 失败: %v", err)
} else if expiredCount > 0 {
glog.Infof(ctx, "清理过期登录 Token 完成,清理数量: %d", expiredCount)
}
glog.Info(ctx, "Token 自动登录任务完成")
return nil
}
// GetTokensNeedingCode 获取需要发送验证码的 Token状态为待验证码
func (s *sCamelOil) GetTokensNeedingCode(ctx context.Context) ([]*entity.V1CamelOilToken, error) {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
var tokens []*entity.V1CamelOilToken
// 获取状态为待验证码的 Token
err := m.Where(dao.V1CamelOilToken.Columns().Status, int(consts.CamelOilTokenStatusPendingVerification)).
Scan(&tokens)
if err != nil {
return nil, gerror.Wrap(err, "查询需要验证码的 Token 失败")
}
return tokens, nil
}
// GetFailedLoginTokens 获取登录失败的 Token状态为登录失败
func (s *sCamelOil) GetFailedLoginTokens(ctx context.Context) ([]*entity.V1CamelOilToken, error) {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
var tokens []*entity.V1CamelOilToken
// 获取状态为登录失败的 Token
err := m.Where(dao.V1CamelOilToken.Columns().Status, int(consts.CamelOilTokenStatusLoginFailed)).
Scan(&tokens)
if err != nil {
return nil, gerror.Wrap(err, "查询登录失败的 Token 失败")
}
return tokens, nil
}
// CleanExpiredLoginTokens 清理登录 token 过期的 Token
func (s *sCamelOil) CleanExpiredLoginTokens(ctx context.Context) (cleanedCount int, err error) {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
// 查询登录 token 已过期但状态仍为可用的 Token
var expiredTokens []*entity.V1CamelOilToken
err = m.Where(dao.V1CamelOilToken.Columns().Status, int(consts.CamelOilTokenStatusAvailable)).
WhereNotNull(dao.V1CamelOilToken.Columns().LoginTokenExpiresAt).
WhereLT(dao.V1CamelOilToken.Columns().LoginTokenExpiresAt, gtime.Now()).
Scan(&expiredTokens)
if err != nil {
return 0, gerror.Wrap(err, "查询过期登录 Token 失败")
}
if len(expiredTokens) == 0 {
return 0, nil
}
// 将这些 Token 标记为需要重新登录
for _, token := range expiredTokens {
_, err = m.Where(dao.V1CamelOilToken.Columns().Id, token.Id).
Update(&do.V1CamelOilToken{
Status: int(consts.CamelOilTokenStatusPendingVerification), // 待验证码状态
LoginToken: "", // 清空登录 token
LoginTokenExpiresAt: nil, // 清空过期时间
UpdatedAt: gtime.Now(),
})
if err != nil {
glog.Warningf(ctx, "标记过期 Token 为待验证码失败ID=%d: %v", token.Id, err)
continue
}
cleanedCount++
}
return cleanedCount, nil
}

View File

@@ -3,6 +3,7 @@ package camel_oil
import (
"context"
"kami/internal/model"
"time"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gcode"
@@ -27,7 +28,7 @@ import (
// ====================================================================================
// CreateToken 创建 Token
func (s *sCamelOil) CreateToken(ctx context.Context, tokenName string, tokenValue string, phone string, remark string, rechargeLimitAmount float64, rechargeLimitCount int) (tokenId int64, err error) {
func (s *sCamelOil) CreateToken(ctx context.Context, input *model.CamelOilTokenCreateInput) (tokenId int64, err error) {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
// 获取当前用户信息
@@ -44,20 +45,19 @@ func (s *sCamelOil) CreateToken(ctx context.Context, tokenName string, tokenValu
}
// 将 float64 转换为 decimal.Decimal 存储到数据库
rechargeLimitAmountDecimal := decimal.NewFromFloat(rechargeLimitAmount)
rechargeLimitAmountDecimal := decimal.NewFromFloat(input.RechargeLimitAmount)
result, err := m.Insert(&do.V1CamelOilToken{
UserId: userId,
TokenName: tokenName,
TokenValue: tokenValue,
Phone: phone,
Status: int(consts.CamelOilTokenStatusAvailable),
Name: input.Name,
Phone: input.Phone,
Status: int(consts.CamelOilTokenStatusPendingVerification), // 待验证码状态
BindCount: 0,
TotalBindAmount: decimal.Zero,
TotalRechargeAmount: decimal.Zero,
RechargeLimitAmount: rechargeLimitAmountDecimal,
RechargeLimitCount: rechargeLimitCount,
Remark: remark,
RechargeLimitCount: input.RechargeLimitCount,
Remark: input.Remark,
})
if err != nil {
@@ -65,7 +65,14 @@ func (s *sCamelOil) CreateToken(ctx context.Context, tokenName string, tokenValu
}
tokenId, _ = result.LastInsertId()
glog.Infof(ctx, "Token创建成功: tokenId=%d, tokenName=%s, phone=%s, 充值金额限制=%.2f, 充值次数限制=%d", tokenId, tokenName, phone, rechargeLimitAmount, rechargeLimitCount)
glog.Infof(ctx, "Token创建成功: tokenId=%d, name=%s, phone=%s, 充值金额限制=%.2f, 充值次数限制=%d", tokenId, input.Name, input.Phone, input.RechargeLimitAmount, input.RechargeLimitCount)
// 异步发送验证码
go func() {
if err := s.SendVerificationCode(ctx, tokenId, input.Phone); err != nil {
glog.Errorf(ctx, "发送验证码失败: tokenId=%d, phone=%s, error=%v", tokenId, input.Phone, err)
}
}()
return tokenId, nil
}
@@ -131,7 +138,7 @@ func (s *sCamelOil) ListTokensWithPagination(ctx context.Context, pageReq common
// 添加查询条件
if tokenName != "" {
query = query.WhereLike(dao.V1CamelOilToken.Columns().TokenName, "%"+tokenName+"%")
query = query.WhereLike(dao.V1CamelOilToken.Columns().Name, "%"+tokenName+"%")
}
if status > 0 {
@@ -163,7 +170,7 @@ func (s *sCamelOil) UpdateToken(ctx context.Context, tokenId int64, tokenName st
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
_, err := m.Where(dao.V1CamelOilToken.Columns().Id, tokenId).Update(&do.V1CamelOilToken{
TokenName: tokenName,
Name: tokenName,
Status: status,
Remark: remark,
UpdatedAt: gtime.Now(),
@@ -206,7 +213,7 @@ func (s *sCamelOil) UpdateTokenInfo(ctx context.Context, tokenId int64, tokenNam
rechargeLimitAmountDecimal := decimal.NewFromFloat(rechargeLimitAmount)
_, err = m.Where(dao.V1CamelOilToken.Columns().Id, tokenId).Update(&do.V1CamelOilToken{
TokenName: tokenName,
Name: tokenName,
Phone: phone,
Remark: remark,
RechargeLimitAmount: rechargeLimitAmountDecimal,
@@ -334,8 +341,8 @@ func (s *sCamelOil) BindCardToToken(ctx context.Context, orderId int64, cardNumb
}
}
// 4.2 调用绑卡接口(使用选中 Token 的 TokenValue
rechargeErrType, rechargeErr := camel_oil_api.NewClient(ctx).RechargeCard(ctx, selectedToken.TokenValue, selectedToken.Phone, cardPassword)
// 4.2 调用绑卡接口(使用选中 Token 的 LoginToken
rechargeErrType, rechargeErr := camel_oil_api.NewClient(ctx).RechargeCard(ctx, selectedToken.LoginToken, selectedToken.Phone, cardPassword)
if rechargeErr != nil {
switch rechargeErrType {
case camel_oil_api.RechargeCardErrorCode:
@@ -488,3 +495,108 @@ func (s *sCamelOil) CalculateTotalBindingAmount(ctx context.Context) (totalAmoun
return result.Total, nil
}
// ====================================================================================
// 验证码和登录相关方法
// ====================================================================================
// SendVerificationCode 发送验证码
func (s *sCamelOil) SendVerificationCode(ctx context.Context, tokenId int64, phone string) error {
client := camel_oil_api.NewClient(ctx)
success, err := client.SendCaptcha(ctx, phone)
if err != nil {
// 更新状态为登录失败
s.UpdateTokenStatus(ctx, tokenId, consts.CamelOilTokenStatusLoginFailed, "发送验证码失败")
return gerror.Wrap(err, "发送验证码失败")
}
if !success {
// 更新状态为登录失败
s.UpdateTokenStatus(ctx, tokenId, consts.CamelOilTokenStatusLoginFailed, "发送验证码失败")
return gerror.New("发送验证码失败")
}
// 更新状态为验证码已发送
s.UpdateTokenStatus(ctx, tokenId, consts.CamelOilTokenStatusCodeSent, "验证码已发送")
glog.Infof(ctx, "验证码发送成功: tokenId=%d, phone=%s", tokenId, phone)
return nil
}
// LoginToken 使用验证码登录 Token
func (s *sCamelOil) LoginToken(ctx context.Context, input *model.CamelOilTokenLoginInput) (string, error) {
// 获取 Token 信息
token, err := s.GetTokenInfo(ctx, input.TokenId)
if err != nil {
return "", gerror.Wrap(err, "查询Token信息失败")
}
if token == nil {
return "", gerror.New("Token不存在")
}
// 检查状态是否已登录
if token.Status == int(consts.CamelOilTokenStatusAvailable) {
return "", gerror.New("Token已登录")
}
client := camel_oil_api.NewClient(ctx)
// 从 Redis 获取验证码(这里简化处理,直接使用传入的验证码)
loginToken, err := client.LoginWithCaptcha(ctx, token.Phone, input.Code)
if err != nil {
// 更新状态为登录失败
s.UpdateTokenStatus(ctx, input.TokenId, consts.CamelOilTokenStatusLoginFailed, "登录失败")
return "", gerror.Wrap(err, "登录失败")
}
// 更新 Token 信息
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
_, err = m.Where(dao.V1CamelOilToken.Columns().Id, input.TokenId).Update(&do.V1CamelOilToken{
LoginToken: loginToken,
LoginTokenExpiresAt: gtime.Now().Add(24 * 3600 * time.Second), // 24小时后过期
LastLoginAt: gtime.Now(),
Status: int(consts.CamelOilTokenStatusAvailable), // 可用状态
UpdatedAt: gtime.Now(),
})
if err != nil {
return "", gerror.Wrap(err, "更新Token登录信息失败")
}
glog.Infof(ctx, "Token登录成功: tokenId=%d, phone=%s", input.TokenId, token.Phone)
return loginToken, nil
}
// ResendVerificationCode 重发验证码
func (s *sCamelOil) ResendVerificationCode(ctx context.Context, input *model.CamelOilTokenResendCodeInput) error {
// 获取 Token 信息
token, err := s.GetTokenInfo(ctx, input.TokenId)
if err != nil {
return gerror.Wrap(err, "查询Token信息失败")
}
if token == nil {
return gerror.New("Token不存在")
}
return s.SendVerificationCode(ctx, input.TokenId, token.Phone)
}
// GetTokensForLogin 获取需要登录的 Token定时任务使用
func (s *sCamelOil) GetTokensForLogin(ctx context.Context) ([]*entity.V1CamelOilToken, error) {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
var tokens []*entity.V1CamelOilToken
// 获取待验证码状态的 Token
err := m.Where(dao.V1CamelOilToken.Columns().Status, int(consts.CamelOilTokenStatusPendingVerification)).
Scan(&tokens)
if err != nil {
return nil, gerror.Wrap(err, "查询待登录Token失败")
}
return tokens, nil
}

View File

@@ -8,3 +8,37 @@ type PrefetchOrderResult struct {
AccountName string // 账号名称
Amount float64 // 订单金额
}
// ====================================================================================
// Token 相关模型
// ====================================================================================
// CamelOilTokenCreateInput 创建 Token 输入结构
type CamelOilTokenCreateInput struct {
Name string `json:"name" v:"required" description:"名称"`
Phone string `json:"phone" v:"required" description:"绑定的手机号"`
Remark string `json:"remark" description:"备注"`
RechargeLimitAmount float64 `json:"rechargeLimitAmount" v:"required" description:"充值金额限制"`
RechargeLimitCount int `json:"rechargeLimitCount" v:"required" description:"充值次数限制"`
}
// CamelOilTokenUpdateInput 更新 Token 输入结构
type CamelOilTokenUpdateInput struct {
TokenId int64 `json:"tokenId" v:"required" description:"Token ID"`
Name string `json:"name" v:"required" description:"名称"`
Phone string `json:"phone" description:"绑定的手机号"`
Remark string `json:"remark" description:"备注"`
RechargeLimitAmount float64 `json:"rechargeLimitAmount" v:"required" description:"充值金额限制"`
RechargeLimitCount int `json:"rechargeLimitCount" v:"required" description:"充值次数限制"`
}
// CamelOilTokenLoginInput Token 登录输入结构
type CamelOilTokenLoginInput struct {
TokenId int64 `json:"tokenId" v:"required" description:"Token ID"`
Code string `json:"code" v:"required" description:"验证码"`
}
// CamelOilTokenResendCodeInput 重发验证码输入结构
type CamelOilTokenResendCodeInput struct {
TokenId int64 `json:"tokenId" v:"required" description:"Token ID"`
}

View File

@@ -14,10 +14,12 @@ type V1CamelOilToken struct {
g.Meta `orm:"table:camel_oil_token, do:true"`
Id any // 主键ID
UserId any // 用户ID空字符串表示管理员创建
TokenName any // Token名称/标识
TokenValue any // Token具体值
Name any // 名称
Phone any // 绑定的手机号
Status any // 状态1可用 2已禁用 3已过期
Status any // 状态:0待验证码 1可用 2已禁用 3已过期 4登录失败
LoginToken any // 登录后获取的token
LoginTokenExpiresAt *gtime.Time // 登录token过期时间
LastLoginAt *gtime.Time // 最后登录时间
BindCount any // 已绑定卡密数量
TotalBindAmount any // 累计绑定金额
LastBindAt *gtime.Time // 最后绑定时间

View File

@@ -11,21 +11,23 @@ import (
// V1CamelOilToken is the golang structure for table v1camel_oil_token.
type V1CamelOilToken struct {
Id int64 `json:"id" orm:"id" description:"主键ID"`
UserId string `json:"userId" orm:"user_id" description:"用户ID空字符串表示管理员创建"`
TokenName string `json:"tokenName" orm:"token_name" description:"Token名称/标识"`
TokenValue string `json:"tokenValue" orm:"token_value" description:"Token具体值"`
Phone string `json:"phone" orm:"phone" description:"绑定的手机号"`
Status int `json:"status" orm:"status" description:"状态1可用 2已禁用 3已过期"`
BindCount int `json:"bindCount" orm:"bind_count" description:"已绑定卡密数量"`
TotalBindAmount decimal.Decimal `json:"totalBindAmount" orm:"total_bind_amount" description:"累计绑定金额"`
LastBindAt *gtime.Time `json:"lastBindAt" orm:"last_bind_at" description:"最后绑定时间"`
LastUsedAt *gtime.Time `json:"lastUsedAt" orm:"last_used_at" 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:"删除时间(软删除)"`
RechargeLimitAmount decimal.Decimal `json:"rechargeLimitAmount" orm:"recharge_limit_amount" description:"充值金额限制"`
RechargeLimitCount int `json:"rechargeLimitCount" orm:"recharge_limit_count" description:"充值次数限制"`
TotalRechargeAmount decimal.Decimal `json:"totalRechargeAmount" orm:"total_recharge_amount" description:"充值金额"`
Id int64 `json:"id" orm:"id" description:"主键ID"`
UserId string `json:"userId" orm:"user_id" description:"用户ID空字符串表示管理员创建"`
Name string `json:"name" orm:"name" description:"名称"`
Phone string `json:"phone" orm:"phone" description:"绑定的手机号"`
Status int `json:"status" orm:"status" description:"状态0待验证码 1可用 2已禁用 3已过期 4登录失败"`
LoginToken string `json:"loginToken" orm:"login_token" description:"登录后获取的token"`
LoginTokenExpiresAt *gtime.Time `json:"loginTokenExpiresAt" orm:"login_token_expires_at" description:"登录token过期时间"`
LastLoginAt *gtime.Time `json:"lastLoginAt" orm:"last_login_at" description:"最后登录时间"`
BindCount int `json:"bindCount" orm:"bind_count" description:"已绑定卡密数量"`
TotalBindAmount decimal.Decimal `json:"totalBindAmount" orm:"total_bind_amount" description:"累计绑定金额"`
LastBindAt *gtime.Time `json:"lastBindAt" orm:"last_bind_at" description:"最后绑定时间"`
LastUsedAt *gtime.Time `json:"lastUsedAt" orm:"last_used_at" 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:"删除时间(软删除)"`
RechargeLimitAmount decimal.Decimal `json:"rechargeLimitAmount" orm:"recharge_limit_amount" description:"充值金额限制"`
RechargeLimitCount int `json:"rechargeLimitCount" orm:"recharge_limit_count" description:"充值次数限制"`
TotalRechargeAmount decimal.Decimal `json:"totalRechargeAmount" orm:"total_recharge_amount" description:"总充值金额"`
}

View File

@@ -73,6 +73,15 @@ type (
CronCardBindingTask(ctx context.Context) error
// CronCleanExpiredPrefetchOrders 清理过期的预拉取订单
CronCleanExpiredPrefetchOrders(ctx context.Context) (cleanedCount int, err error)
// CronTokenLoginTask Token 自动登录定时任务 - 由cron调度器定期调用
// 流程:检查需要发送验证码或重新登录的 Token自动处理登录流程
CronTokenLoginTask(ctx context.Context) error
// GetTokensNeedingCode 获取需要发送验证码的 Token状态为待验证码
GetTokensNeedingCode(ctx context.Context) ([]*entity.V1CamelOilToken, error)
// GetFailedLoginTokens 获取登录失败的 Token状态为登录失败
GetFailedLoginTokens(ctx context.Context) ([]*entity.V1CamelOilToken, error)
// CleanExpiredLoginTokens 清理登录 token 过期的 Token
CleanExpiredLoginTokens(ctx context.Context) (cleanedCount int, err error)
// UpdateOrderStatus 更新订单状态并记录历史
UpdateOrderStatus(ctx context.Context, orderId int64, newStatus consts.CamelOilOrderStatus, operationType consts.CamelOilOrderChangeType, rawData string, description string) (err error)
// SubmitOrder 提交订单并返回支付宝支付链接
@@ -116,7 +125,7 @@ type (
// GetCamelOilSettings 获取骆驼模块设置的辅助函数
GetCamelOilSettings(ctx context.Context) (*v1.CamelOilSettings, error)
// CreateToken 创建 Token
CreateToken(ctx context.Context, tokenName string, tokenValue string, phone string, remark string, rechargeLimitAmount float64, rechargeLimitCount int) (tokenId int64, err error)
CreateToken(ctx context.Context, input *model.CamelOilTokenCreateInput) (tokenId int64, err error)
// GetTokenInfo 获取 Token 信息
GetTokenInfo(ctx context.Context, tokenId int64) (token *entity.V1CamelOilToken, err error)
// ListTokens 列出所有可用的 Token
@@ -143,6 +152,14 @@ type (
GetTokenBindingStats(ctx context.Context, tokenId int64) (bindCount int, totalAmount decimal.Decimal, err error)
// CalculateTotalBindingAmount 计算所有 Token 的累计绑定金额
CalculateTotalBindingAmount(ctx context.Context) (totalAmount decimal.Decimal, err error)
// SendVerificationCode 发送验证码
SendVerificationCode(ctx context.Context, tokenId int64, phone string) error
// LoginToken 使用验证码登录 Token
LoginToken(ctx context.Context, input *model.CamelOilTokenLoginInput) (string, error)
// ResendVerificationCode 重发验证码
ResendVerificationCode(ctx context.Context, input *model.CamelOilTokenResendCodeInput) error
// GetTokensForLogin 获取需要登录的 Token定时任务使用
GetTokensForLogin(ctx context.Context) ([]*entity.V1CamelOilToken, error)
}
)