diff --git a/app/admin/apis/sms_abnormal_number.go b/app/admin/apis/sms_abnormal_number.go new file mode 100644 index 0000000..aa85b14 --- /dev/null +++ b/app/admin/apis/sms_abnormal_number.go @@ -0,0 +1,214 @@ +package apis + +import ( + "fmt" + + "github.com/gin-gonic/gin" + "github.com/go-admin-team/go-admin-core/sdk/api" + "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user" + _ "github.com/go-admin-team/go-admin-core/sdk/pkg/response" + + "go-admin/app/admin/models" + "go-admin/app/admin/service" + "go-admin/app/admin/service/dto" + "go-admin/common/actions" +) + +type SmsAbnormalNumber struct { + api.Api +} + +// GetPage 获取异常号码统计列表 +// @Summary 获取异常号码统计列表 +// @Description 获取异常号码统计列表 +// @Tags 异常号码统计 +// @Param platformCode query string false "平台code" +// @Param phone query string false "电话号码" +// @Param pageSize query int false "页条数" +// @Param pageIndex query int false "页码" +// @Success 200 {object} response.Response{data=response.Page{list=[]models.SmsAbnormalNumber}} "{"code": 200, "data": [...]}" +// @Router /api/v1/sms-abnormal-number [get] +// @Security Bearer +func (e SmsAbnormalNumber) GetPage(c *gin.Context) { + req := dto.SmsAbnormalNumberGetPageReq{} + s := service.SmsAbnormalNumber{} + err := e.MakeContext(c). + MakeOrm(). + Bind(&req). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + + p := actions.GetPermissionFromContext(c) + list := make([]models.SmsAbnormalNumber, 0) + var count int64 + + err = s.GetPage(&req, p, &list, &count) + if err != nil { + e.Error(500, err, fmt.Sprintf("获取异常号码统计失败,\r\n失败信息 %s", err.Error())) + return + } + + e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功") +} + +// Get 获取异常号码统计 +// @Summary 获取异常号码统计 +// @Description 获取异常号码统计 +// @Tags 异常号码统计 +// @Param id path int false "id" +// @Success 200 {object} response.Response{data=models.SmsAbnormalNumber} "{"code": 200, "data": [...]}" +// @Router /api/v1/sms-abnormal-number/{id} [get] +// @Security Bearer +func (e SmsAbnormalNumber) Get(c *gin.Context) { + req := dto.SmsAbnormalNumberGetReq{} + s := service.SmsAbnormalNumber{} + err := e.MakeContext(c). + MakeOrm(). + Bind(&req). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + var object models.SmsAbnormalNumber + + p := actions.GetPermissionFromContext(c) + err = s.Get(&req, p, &object) + if err != nil { + e.Error(500, err, fmt.Sprintf("获取异常号码统计失败,\r\n失败信息 %s", err.Error())) + return + } + + e.OK(object, "查询成功") +} + +// Insert 创建异常号码统计 +// @Summary 创建异常号码统计 +// @Description 创建异常号码统计 +// @Tags 异常号码统计 +// @Accept application/json +// @Product application/json +// @Param data body dto.SmsAbnormalNumberInsertReq true "data" +// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}" +// @Router /api/v1/sms-abnormal-number [post] +// @Security Bearer +func (e SmsAbnormalNumber) Insert(c *gin.Context) { + req := dto.SmsAbnormalNumberInsertReq{} + s := service.SmsAbnormalNumber{} + err := e.MakeContext(c). + MakeOrm(). + Bind(&req). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + // 设置创建人 + req.SetCreateBy(user.GetUserId(c)) + + err = s.Insert(&req) + if err != nil { + e.Error(500, err, fmt.Sprintf("创建异常号码统计失败,\r\n失败信息 %s", err.Error())) + return + } + + e.OK(req.GetId(), "创建成功") +} + +// Update 修改异常号码统计 +// @Summary 修改异常号码统计 +// @Description 修改异常号码统计 +// @Tags 异常号码统计 +// @Accept application/json +// @Product application/json +// @Param id path int true "id" +// @Param data body dto.SmsAbnormalNumberUpdateReq true "body" +// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}" +// @Router /api/v1/sms-abnormal-number/{id} [put] +// @Security Bearer +func (e SmsAbnormalNumber) Update(c *gin.Context) { + req := dto.SmsAbnormalNumberUpdateReq{} + s := service.SmsAbnormalNumber{} + err := e.MakeContext(c). + MakeOrm(). + Bind(&req). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + req.SetUpdateBy(user.GetUserId(c)) + p := actions.GetPermissionFromContext(c) + + err = s.Update(&req, p) + if err != nil { + e.Error(500, err, fmt.Sprintf("修改异常号码统计失败,\r\n失败信息 %s", err.Error())) + return + } + e.OK(req.GetId(), "修改成功") +} + +// Delete 删除异常号码统计 +// @Summary 删除异常号码统计 +// @Description 删除异常号码统计 +// @Tags 异常号码统计 +// @Param data body dto.SmsAbnormalNumberDeleteReq true "body" +// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}" +// @Router /api/v1/sms-abnormal-number [delete] +// @Security Bearer +func (e SmsAbnormalNumber) Delete(c *gin.Context) { + s := service.SmsAbnormalNumber{} + req := dto.SmsAbnormalNumberDeleteReq{} + err := e.MakeContext(c). + MakeOrm(). + Bind(&req). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + + // req.SetUpdateBy(user.GetUserId(c)) + p := actions.GetPermissionFromContext(c) + + err = s.Remove(&req, p) + if err != nil { + e.Error(500, err, fmt.Sprintf("删除异常号码统计失败,\r\n失败信息 %s", err.Error())) + return + } + e.OK(req.GetId(), "删除成功") +} + +// 同步平台和系统的差异 +func (e SmsAbnormalNumber) SyncState(c *gin.Context) { + s := service.SmsAbnormalNumber{} + err := e.MakeContext(c). + MakeOrm(). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + + err = s.SyncState() + if err != nil { + e.Error(500, err, fmt.Sprintf("同步异常号码统计失败,\r\n失败信息 %s", err.Error())) + return + } + e.OK(nil, "同步成功") +} diff --git a/app/admin/apis/sms_platform_key.go b/app/admin/apis/sms_platform_key.go new file mode 100644 index 0000000..f1b74a4 --- /dev/null +++ b/app/admin/apis/sms_platform_key.go @@ -0,0 +1,192 @@ +package apis + +import ( + "fmt" + + "github.com/gin-gonic/gin" + "github.com/go-admin-team/go-admin-core/sdk/api" + "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user" + _ "github.com/go-admin-team/go-admin-core/sdk/pkg/response" + + "go-admin/app/admin/models" + "go-admin/app/admin/service" + "go-admin/app/admin/service/dto" + "go-admin/common/actions" +) + +type SmsPlatformKey struct { + api.Api +} + +// GetPage 获取平台密钥管理列表 +// @Summary 获取平台密钥管理列表 +// @Description 获取平台密钥管理列表 +// @Tags 平台密钥管理 +// @Param platformCode query string false "平台code" +// @Param pageSize query int false "页条数" +// @Param pageIndex query int false "页码" +// @Success 200 {object} response.Response{data=response.Page{list=[]models.SmsPlatformKey}} "{"code": 200, "data": [...]}" +// @Router /api/v1/sms-platform-key [get] +// @Security Bearer +func (e SmsPlatformKey) GetPage(c *gin.Context) { + req := dto.SmsPlatformKeyGetPageReq{} + s := service.SmsPlatformKey{} + err := e.MakeContext(c). + MakeOrm(). + Bind(&req). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + + p := actions.GetPermissionFromContext(c) + list := make([]models.SmsPlatformKey, 0) + var count int64 + + err = s.GetPage(&req, p, &list, &count) + if err != nil { + e.Error(500, err, fmt.Sprintf("获取平台密钥管理失败,\r\n失败信息 %s", err.Error())) + return + } + + e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功") +} + +// Get 获取平台密钥管理 +// @Summary 获取平台密钥管理 +// @Description 获取平台密钥管理 +// @Tags 平台密钥管理 +// @Param id path int false "id" +// @Success 200 {object} response.Response{data=models.SmsPlatformKey} "{"code": 200, "data": [...]}" +// @Router /api/v1/sms-platform-key/{id} [get] +// @Security Bearer +func (e SmsPlatformKey) Get(c *gin.Context) { + req := dto.SmsPlatformKeyGetReq{} + s := service.SmsPlatformKey{} + err := e.MakeContext(c). + MakeOrm(). + Bind(&req). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + var object models.SmsPlatformKey + + p := actions.GetPermissionFromContext(c) + err = s.Get(&req, p, &object) + if err != nil { + e.Error(500, err, fmt.Sprintf("获取平台密钥管理失败,\r\n失败信息 %s", err.Error())) + return + } + + e.OK(object, "查询成功") +} + +// Insert 创建平台密钥管理 +// @Summary 创建平台密钥管理 +// @Description 创建平台密钥管理 +// @Tags 平台密钥管理 +// @Accept application/json +// @Product application/json +// @Param data body dto.SmsPlatformKeyInsertReq true "data" +// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}" +// @Router /api/v1/sms-platform-key [post] +// @Security Bearer +func (e SmsPlatformKey) Insert(c *gin.Context) { + req := dto.SmsPlatformKeyInsertReq{} + s := service.SmsPlatformKey{} + err := e.MakeContext(c). + MakeOrm(). + Bind(&req). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + // 设置创建人 + req.SetCreateBy(user.GetUserId(c)) + + err = s.Insert(&req) + if err != nil { + e.Error(500, err, fmt.Sprintf("创建平台密钥管理失败,\r\n失败信息 %s", err.Error())) + return + } + + e.OK(req.GetId(), "创建成功") +} + +// Update 修改平台密钥管理 +// @Summary 修改平台密钥管理 +// @Description 修改平台密钥管理 +// @Tags 平台密钥管理 +// @Accept application/json +// @Product application/json +// @Param id path int true "id" +// @Param data body dto.SmsPlatformKeyUpdateReq true "body" +// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}" +// @Router /api/v1/sms-platform-key/{id} [put] +// @Security Bearer +func (e SmsPlatformKey) Update(c *gin.Context) { + req := dto.SmsPlatformKeyUpdateReq{} + s := service.SmsPlatformKey{} + err := e.MakeContext(c). + MakeOrm(). + Bind(&req). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + req.SetUpdateBy(user.GetUserId(c)) + p := actions.GetPermissionFromContext(c) + + err = s.Update(&req, p) + if err != nil { + e.Error(500, err, fmt.Sprintf("修改平台密钥管理失败,\r\n失败信息 %s", err.Error())) + return + } + e.OK(req.GetId(), "修改成功") +} + +// Delete 删除平台密钥管理 +// @Summary 删除平台密钥管理 +// @Description 删除平台密钥管理 +// @Tags 平台密钥管理 +// @Param data body dto.SmsPlatformKeyDeleteReq true "body" +// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}" +// @Router /api/v1/sms-platform-key [delete] +// @Security Bearer +func (e SmsPlatformKey) Delete(c *gin.Context) { + s := service.SmsPlatformKey{} + req := dto.SmsPlatformKeyDeleteReq{} + err := e.MakeContext(c). + MakeOrm(). + Bind(&req). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + + // req.SetUpdateBy(user.GetUserId(c)) + p := actions.GetPermissionFromContext(c) + + err = s.Remove(&req, p) + if err != nil { + e.Error(500, err, fmt.Sprintf("删除平台密钥管理失败,\r\n失败信息 %s", err.Error())) + return + } + e.OK(req.GetId(), "删除成功") +} diff --git a/app/admin/models/sms_abnormal_number.go b/app/admin/models/sms_abnormal_number.go new file mode 100644 index 0000000..3f75385 --- /dev/null +++ b/app/admin/models/sms_abnormal_number.go @@ -0,0 +1,30 @@ +package models + +import ( + + "go-admin/common/models" + +) + +type SmsAbnormalNumber struct { + models.Model + + Account string `json:"account" gorm:"type:varchar(255);comment:账号"` + PlatformCode string `json:"platformCode" gorm:"type:varchar(20);comment:平台code"` + Phone string `json:"phone" gorm:"type:varchar(30);comment:电话号码"` + models.ModelTime + models.ControlBy +} + +func (SmsAbnormalNumber) TableName() string { + return "sms_abnormal_number" +} + +func (e *SmsAbnormalNumber) Generate() models.ActiveRecord { + o := *e + return &o +} + +func (e *SmsAbnormalNumber) GetId() interface{} { + return e.Id +} \ No newline at end of file diff --git a/app/admin/models/sms_phone.go b/app/admin/models/sms_phone.go index 7115e06..4bedcbd 100644 --- a/app/admin/models/sms_phone.go +++ b/app/admin/models/sms_phone.go @@ -12,6 +12,7 @@ type SmsPhone struct { PlatformCode string `json:"platformCode" gorm:"type:varchar(20);comment:平台code"` UserId int `json:"userId" gorm:"type:bigint;comment:用户Id"` + ApiKey string `json:"apiKey" gorm:"type:varchar(255);comment:apiKey"` Service string `json:"service" gorm:"type:varchar(50);comment:sms 服务"` ServiceCode string `json:"serviceCode" gorm:"type:varchar(30);comment:服务code"` Type int `json:"type" gorm:"type:tinyint;comment:类型 0-短效 1-长效"` diff --git a/app/admin/models/sms_platform_key.go b/app/admin/models/sms_platform_key.go new file mode 100644 index 0000000..471a725 --- /dev/null +++ b/app/admin/models/sms_platform_key.go @@ -0,0 +1,31 @@ +package models + +import ( + "go-admin/common/models" +) + +type SmsPlatformKey struct { + models.Model + + PlatformCode string `json:"platformCode" gorm:"type:varchar(20);comment:平台code"` + Account string `json:"account" gorm:"type:varchar(50);comment:账号"` + ApiKey string `json:"apiKey" gorm:"type:varchar(500);comment:平台key"` + ApiSecret string `json:"apiSecret" gorm:"type:varchar(255);comment:平台私钥"` + Status int64 `json:"status" gorm:"type:tinyint;comment:状态 1-启用 2-禁用"` + Remark string `json:"remark" gorm:"type:varchar(255);comment:备注"` + models.ModelTime + models.ControlBy +} + +func (SmsPlatformKey) TableName() string { + return "sms_platform_key" +} + +func (e *SmsPlatformKey) Generate() models.ActiveRecord { + o := *e + return &o +} + +func (e *SmsPlatformKey) GetId() interface{} { + return e.Id +} diff --git a/app/admin/router/sms_abnormal_number.go b/app/admin/router/sms_abnormal_number.go new file mode 100644 index 0000000..c39efba --- /dev/null +++ b/app/admin/router/sms_abnormal_number.go @@ -0,0 +1,29 @@ +package router + +import ( + "github.com/gin-gonic/gin" + jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" + + "go-admin/app/admin/apis" + "go-admin/common/actions" + "go-admin/common/middleware" +) + +func init() { + routerCheckRole = append(routerCheckRole, registerSmsAbnormalNumberRouter) +} + +// registerSmsAbnormalNumberRouter +func registerSmsAbnormalNumberRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) { + api := apis.SmsAbnormalNumber{} + r := v1.Group("/sms-abnormal-number").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole()) + { + r.GET("", actions.PermissionAction(), api.GetPage) + r.GET("/:id", actions.PermissionAction(), api.Get) + r.POST("", api.Insert) + r.PUT("/:id", actions.PermissionAction(), api.Update) + r.DELETE("", api.Delete) + + r.POST("/sync-state", api.SyncState) //同步差异 + } +} diff --git a/app/admin/router/sms_platform_key.go b/app/admin/router/sms_platform_key.go new file mode 100644 index 0000000..72a6f07 --- /dev/null +++ b/app/admin/router/sms_platform_key.go @@ -0,0 +1,27 @@ +package router + +import ( + "github.com/gin-gonic/gin" + jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" + + "go-admin/app/admin/apis" + "go-admin/common/middleware" + "go-admin/common/actions" +) + +func init() { + routerCheckRole = append(routerCheckRole, registerSmsPlatformKeyRouter) +} + +// registerSmsPlatformKeyRouter +func registerSmsPlatformKeyRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) { + api := apis.SmsPlatformKey{} + r := v1.Group("/sms-platform-key").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole()) + { + r.GET("", actions.PermissionAction(), api.GetPage) + r.GET("/:id", actions.PermissionAction(), api.Get) + r.POST("", api.Insert) + r.PUT("/:id", actions.PermissionAction(), api.Update) + r.DELETE("", api.Delete) + } +} \ No newline at end of file diff --git a/app/admin/service/dto/sms_abnormal_number.go b/app/admin/service/dto/sms_abnormal_number.go new file mode 100644 index 0000000..0c3ee72 --- /dev/null +++ b/app/admin/service/dto/sms_abnormal_number.go @@ -0,0 +1,93 @@ +package dto + +import ( + + "go-admin/app/admin/models" + "go-admin/common/dto" + common "go-admin/common/models" +) + +type SmsAbnormalNumberGetPageReq struct { + dto.Pagination `search:"-"` + PlatformCode string `form:"platformCode" search:"type:exact;column:platform_code;table:sms_abnormal_number" comment:"平台code"` + Phone string `form:"phone" search:"type:contains;column:phone;table:sms_abnormal_number" comment:"电话号码"` + SmsAbnormalNumberOrder +} + +type SmsAbnormalNumberOrder struct { + Id string `form:"idOrder" search:"type:order;column:id;table:sms_abnormal_number"` + Account string `form:"accountOrder" search:"type:order;column:account;table:sms_abnormal_number"` + PlatformCode string `form:"platformCodeOrder" search:"type:order;column:platform_code;table:sms_abnormal_number"` + Phone string `form:"phoneOrder" search:"type:order;column:phone;table:sms_abnormal_number"` + CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:sms_abnormal_number"` + UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:sms_abnormal_number"` + DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:sms_abnormal_number"` + CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:sms_abnormal_number"` + UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:sms_abnormal_number"` + +} + +func (m *SmsAbnormalNumberGetPageReq) GetNeedSearch() interface{} { + return *m +} + +type SmsAbnormalNumberInsertReq struct { + Id int `json:"-" comment:"主键id"` // 主键id + Account string `json:"account" comment:"账号"` + PlatformCode string `json:"platformCode" comment:"平台code"` + Phone string `json:"phone" comment:"电话号码"` + common.ControlBy +} + +func (s *SmsAbnormalNumberInsertReq) Generate(model *models.SmsAbnormalNumber) { + if s.Id == 0 { + model.Model = common.Model{ Id: s.Id } + } + model.Account = s.Account + model.PlatformCode = s.PlatformCode + model.Phone = s.Phone + model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的 +} + +func (s *SmsAbnormalNumberInsertReq) GetId() interface{} { + return s.Id +} + +type SmsAbnormalNumberUpdateReq struct { + Id int `uri:"id" comment:"主键id"` // 主键id + Account string `json:"account" comment:"账号"` + PlatformCode string `json:"platformCode" comment:"平台code"` + Phone string `json:"phone" comment:"电话号码"` + common.ControlBy +} + +func (s *SmsAbnormalNumberUpdateReq) Generate(model *models.SmsAbnormalNumber) { + if s.Id == 0 { + model.Model = common.Model{ Id: s.Id } + } + model.Account = s.Account + model.PlatformCode = s.PlatformCode + model.Phone = s.Phone + model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的 +} + +func (s *SmsAbnormalNumberUpdateReq) GetId() interface{} { + return s.Id +} + +// SmsAbnormalNumberGetReq 功能获取请求参数 +type SmsAbnormalNumberGetReq struct { + Id int `uri:"id"` +} +func (s *SmsAbnormalNumberGetReq) GetId() interface{} { + return s.Id +} + +// SmsAbnormalNumberDeleteReq 功能删除请求参数 +type SmsAbnormalNumberDeleteReq struct { + Ids []int `json:"ids"` +} + +func (s *SmsAbnormalNumberDeleteReq) GetId() interface{} { + return s.Ids +} diff --git a/app/admin/service/dto/sms_platform_key.go b/app/admin/service/dto/sms_platform_key.go new file mode 100644 index 0000000..f337f0e --- /dev/null +++ b/app/admin/service/dto/sms_platform_key.go @@ -0,0 +1,117 @@ +package dto + +import ( + "go-admin/app/admin/models" + "go-admin/common/dto" + common "go-admin/common/models" +) + +type SmsPlatformKeyGetPageReq struct { + dto.Pagination `search:"-"` + PlatformCode string `form:"platformCode" search:"type:exact;column:platform_code;table:sms_platform_key" comment:"平台code"` + SmsPlatformKeyOrder +} + +type SmsPlatformKeyOrder struct { + Id string `form:"idOrder" search:"type:order;column:id;table:sms_platform_key"` + PlatformCode string `form:"platformCodeOrder" search:"type:order;column:platform_code;table:sms_platform_key"` + ApiKey string `form:"apiKeyOrder" search:"type:order;column:api_key;table:sms_platform_key"` + ApiSecret string `form:"apiSecretOrder" search:"type:order;column:api_secret;table:sms_platform_key"` + Status string `form:"statusOrder" search:"type:order;column:status;table:sms_platform_key"` + Remark string `form:"remarkOrder" search:"type:order;column:remark;table:sms_platform_key"` + CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:sms_platform_key"` + UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:sms_platform_key"` + DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:sms_platform_key"` + CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:sms_platform_key"` + UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:sms_platform_key"` +} + +func (m *SmsPlatformKeyGetPageReq) GetNeedSearch() interface{} { + return *m +} + +type SmsPlatformKeyInsertReq struct { + Id int `json:"-" comment:"主键id"` // 主键id + PlatformCode string `json:"platformCode" comment:"平台code"` + Account string `json:"account" comment:"account"` + ApiKey string `json:"apiKey" comment:"平台key"` + ApiSecret string `json:"apiSecret" comment:"平台私钥"` + Status int64 `json:"status" comment:"状态 1-启用 2-禁用"` + Remark string `json:"remark" comment:"备注"` + common.ControlBy +} + +func (s *SmsPlatformKeyInsertReq) Generate(model *models.SmsPlatformKey) { + if s.Id == 0 { + model.Model = common.Model{Id: s.Id} + } + model.PlatformCode = s.PlatformCode + model.Account = s.Account + model.ApiKey = s.ApiKey + model.ApiSecret = s.ApiSecret + model.Status = s.Status + model.Remark = s.Remark + model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的 +} + +func (s *SmsPlatformKeyInsertReq) GetId() interface{} { + return s.Id +} + +type SmsPlatformKeyUpdateReq struct { + Id int `uri:"id" comment:"主键id"` // 主键id + PlatformCode string `json:"platformCode" comment:"平台code"` + Account string `json:"account" comment:"平台账号"` + ApiKey string `json:"apiKey" comment:"平台key"` + ApiSecret string `json:"apiSecret" comment:"平台私钥"` + Status int64 `json:"status" comment:"状态 1-启用 2-禁用"` + Remark string `json:"remark" comment:"备注"` + common.ControlBy +} + +func (s *SmsPlatformKeyUpdateReq) Generate(model *models.SmsPlatformKey) { + if s.Id == 0 { + model.Model = common.Model{Id: s.Id} + } + model.PlatformCode = s.PlatformCode + model.Account = s.Account + model.ApiKey = s.ApiKey + model.ApiSecret = s.ApiSecret + model.Status = s.Status + model.Remark = s.Remark + model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的 +} + +func (s *SmsPlatformKeyUpdateReq) GetId() interface{} { + return s.Id +} + +// SmsPlatformKeyGetReq 功能获取请求参数 +type SmsPlatformKeyGetReq struct { + Id int `uri:"id"` +} + +func (s *SmsPlatformKeyGetReq) GetId() interface{} { + return s.Id +} + +// SmsPlatformKeyDeleteReq 功能删除请求参数 +type SmsPlatformKeyDeleteReq struct { + Ids []int `json:"ids"` +} + +func (s *SmsPlatformKeyDeleteReq) GetId() interface{} { + return s.Ids +} + +type SmsPlatformKeyQueueDto struct { + PlatformCode string `json:"platformCode" comment:"平台code"` + Account string `json:"account" comment:"平台账号"` + ApiKey string `json:"apiKey" comment:"平台key"` + ApiSecret string `json:"apiSecret" comment:"平台私钥"` +} + +type SmsPlatformKeyGroupDto struct { + PlatformCode string `json:"platformCode"` + Count int `json:"count"` +} diff --git a/app/admin/service/dto/text_verified.go b/app/admin/service/dto/text_verified.go index c57f8bf..58e120c 100644 --- a/app/admin/service/dto/text_verified.go +++ b/app/admin/service/dto/text_verified.go @@ -152,8 +152,9 @@ type VerificationDTO struct { // * nonrenewableActive: 服务活跃中。 // * nonrenewableExpired: 服务已过期。 // * nonrenewableRefunded: 服务已退款。 - State string `json:"state" comment:"verificationPending┃verificationCompleted┃verificationCanceled┃verificationTimedOut┃verificationReported┃verificationRefunded┃verificationReused┃verificationReactivated┃renewableActive┃renewableOverdue┃renewableExpired┃renewableRefunded┃nonrenewableActive┃nonrenewableExpired┃nonrenewableRefunded"` - TotalCost float64 `json:"totalCost"` + State string `json:"state" comment:"verificationPending┃verificationCompleted┃verificationCanceled┃verificationTimedOut┃verificationReported┃verificationRefunded┃verificationReused┃verificationReactivated┃renewableActive┃renewableOverdue┃renewableExpired┃renewableRefunded┃nonrenewableActive┃nonrenewableExpired┃nonrenewableRefunded"` + TotalCost float64 `json:"totalCost"` + IsIncludedForNextRenewal bool `json:"isIncludedForNextRenewal" comment:"是否自动续期"` } // 长效详情 diff --git a/app/admin/service/sms_abnormal_number.go b/app/admin/service/sms_abnormal_number.go new file mode 100644 index 0000000..618386d --- /dev/null +++ b/app/admin/service/sms_abnormal_number.go @@ -0,0 +1,250 @@ +package service + +import ( + "errors" + "time" + + "github.com/go-admin-team/go-admin-core/sdk/service" + "gorm.io/gorm" + + "go-admin/app/admin/models" + "go-admin/app/admin/service/dto" + "go-admin/common/actions" + cDto "go-admin/common/dto" + "go-admin/common/global" +) + +type SmsAbnormalNumber struct { + service.Service +} + +// GetPage 获取SmsAbnormalNumber列表 +func (e *SmsAbnormalNumber) GetPage(c *dto.SmsAbnormalNumberGetPageReq, p *actions.DataPermission, list *[]models.SmsAbnormalNumber, count *int64) error { + var err error + var data models.SmsAbnormalNumber + + err = e.Orm.Model(&data). + Scopes( + cDto.MakeCondition(c.GetNeedSearch()), + cDto.Paginate(c.GetPageSize(), c.GetPageIndex()), + actions.Permission(data.TableName(), p), + ). + Find(list).Limit(-1).Offset(-1). + Count(count).Error + if err != nil { + e.Log.Errorf("SmsAbnormalNumberService GetPage error:%s \r\n", err) + return err + } + return nil +} + +// Get 获取SmsAbnormalNumber对象 +func (e *SmsAbnormalNumber) Get(d *dto.SmsAbnormalNumberGetReq, p *actions.DataPermission, model *models.SmsAbnormalNumber) error { + var data models.SmsAbnormalNumber + + err := e.Orm.Model(&data). + Scopes( + actions.Permission(data.TableName(), p), + ). + First(model, d.GetId()).Error + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + err = errors.New("查看对象不存在或无权查看") + e.Log.Errorf("Service GetSmsAbnormalNumber error:%s \r\n", err) + return err + } + if err != nil { + e.Log.Errorf("db error:%s", err) + return err + } + return nil +} + +// Insert 创建SmsAbnormalNumber对象 +func (e *SmsAbnormalNumber) Insert(c *dto.SmsAbnormalNumberInsertReq) error { + var err error + var data models.SmsAbnormalNumber + c.Generate(&data) + err = e.Orm.Create(&data).Error + if err != nil { + e.Log.Errorf("SmsAbnormalNumberService Insert error:%s \r\n", err) + return err + } + return nil +} + +// Update 修改SmsAbnormalNumber对象 +func (e *SmsAbnormalNumber) Update(c *dto.SmsAbnormalNumberUpdateReq, p *actions.DataPermission) error { + var err error + var data = models.SmsAbnormalNumber{} + e.Orm.Scopes( + actions.Permission(data.TableName(), p), + ).First(&data, c.GetId()) + c.Generate(&data) + + db := e.Orm.Save(&data) + if err = db.Error; err != nil { + e.Log.Errorf("SmsAbnormalNumberService Save error:%s \r\n", err) + return err + } + if db.RowsAffected == 0 { + return errors.New("无权更新该数据") + } + return nil +} + +// Remove 删除SmsAbnormalNumber +func (e *SmsAbnormalNumber) Remove(d *dto.SmsAbnormalNumberDeleteReq, p *actions.DataPermission) error { + var data models.SmsAbnormalNumber + + db := e.Orm.Model(&data). + Scopes( + actions.Permission(data.TableName(), p), + ).Delete(&data, d.GetId()) + if err := db.Error; err != nil { + e.Log.Errorf("Service RemoveSmsAbnormalNumber error:%s \r\n", err) + return err + } + if db.RowsAffected == 0 { + return errors.New("无权删除该数据") + } + return nil +} + +// 获取平台上不再系统内的自动续期号码 +func (e *SmsAbnormalNumber) SyncState() error { + textVerified := SmsTextVerified{Service: e.Service} + // daiSysms := SmsDaisysms{Service: e.Service} 没有列表api + + textVerifiedNumbers, err := textVerified.GetAllPlatformNumbers() + + if err != nil { + e.Log.Errorf("获取长效号码列表失败 %v", err) + } else { + // 获取系统内textverified平台的自动续期号码 + // 参数: 平台代码, 号码类型(1=长效), 自动续费(1=开启), 激活状态(2=已激活) + systemNumbers, err := e.GetPhoneByPlatformCode(global.SmsPlatformTextVerified, 1, 1, 2) + if err != nil { + e.Log.Errorf("获取系统内textverified自动续期号码失败: %v", err) + return err + } + + e.Log.Infof("系统内textverified自动续期号码数量: %d", len(systemNumbers)) + + // 比较平台号码和系统号码,找出差异 + missingNumbers := e.findMissingNumbers(textVerifiedNumbers, systemNumbers) + + if len(missingNumbers) > 0 { + e.Log.Warnf("发现%d个平台开启自动续费但系统内缺失的号码", len(missingNumbers)) + + err = e.Orm.Transaction(func(tx *gorm.DB) error { + if err1 := tx.Model(&models.SmsAbnormalNumber{}). + Unscoped(). + Where(" platform_code =?", global.SmsPlatformTextVerified). + Delete(&models.SmsAbnormalNumber{}). + Error; err1 != nil { + return err1 + } + + if err1 := tx.CreateInBatches(missingNumbers, 100).Error; err1 != nil { + return err1 + } + + return nil + }) + + if err != nil { + e.Log.Errorf("同步后保存差异数据失败 %v", err) + } + } else { + e.Log.Infof("所有平台自动续期号码在系统内都存在") + } + } + + return nil +} + +// findMissingNumbers 比较平台号码和系统号码,找出平台上有但系统内没有的号码 +// 参数: +// - platformNumbers: 平台上的号码列表 +// - systemNumbers: 系统内的号码列表 +// +// 返回: +// - []dto.VerificationDTO: 平台上有但系统内缺失的号码列表 +func (e *SmsAbnormalNumber) findMissingNumbers(platformNumbers map[string][]dto.VerificationDTO, systemNumbers []models.SmsPhone) []models.SmsAbnormalNumber { + // 创建系统号码的映射表,用于快速查找 + systemNumberMap := make(map[string]bool) + for _, sysPhone := range systemNumbers { + // 使用号码作为key + systemNumberMap[sysPhone.Phone] = true + } + + // 找出平台上有但系统内没有的号码 + missingNumbers := make([]models.SmsAbnormalNumber, 0) + for account, platformPhone := range platformNumbers { + for _, v := range platformPhone { + if v.State == "renewableActive" && v.IsIncludedForNextRenewal && !systemNumberMap[v.Number] { + missingNumbers = append(missingNumbers, models.SmsAbnormalNumber{ + PlatformCode: global.SmsPlatformTextVerified, + Account: account, + Phone: v.Number, + }) + } + } + } + + return missingNumbers +} + +// GetPhoneByPlatformCode 循环获取对应平台自动续期的号码 +// 为避免一次性查询过多数据,使用分页查询,每次最多查询100条记录 +// @param platformCode 平台代码 (如: textverified, daisysms) +// @return []models.SmsPhone 号码列表 +// @return error 错误信息 +func (e *SmsAbnormalNumber) GetPhoneByPlatformCode(platformCode string, numberType, autoRenewal, status int) ([]models.SmsPhone, error) { + var phones []models.SmsPhone + var allPhones []models.SmsPhone + + // 分页参数 + pageSize := 1000 // 每页最多1000条记录 + offset := 0 + + // 循环分页查询,避免一次性查询过多数据 + for { + // 查询条件: + // 1. platform_code = platformCode (指定平台) + // 2. type = numberType (号码类型) + // 3. auto_renewal = autoRenewal (自动续费状态) + // 4. actived = status (激活状态) + err := e.Orm.Model(&models.SmsPhone{}). + Where("platform_code = ? AND type = ? AND auto_renewal = ? AND actived = ? AND expire_time > ?", + platformCode, numberType, autoRenewal, status, time.Now()). + Order("id ASC"). + Limit(pageSize). + Offset(offset). + Find(&phones).Error + + if err != nil { + e.Log.Errorf("查询平台[%s]自动续期号码失败: %v", platformCode, err) + return nil, err + } + + // 如果没有查询到数据,说明已经查询完毕 + if len(phones) == 0 { + break + } + + // 将本次查询结果添加到总结果中 + allPhones = append(allPhones, phones...) + + // 如果本次查询结果少于pageSize,说明已经是最后一页 + if len(phones) < pageSize { + break + } + + // 更新偏移量,准备查询下一页 + offset += pageSize + } + + e.Log.Infof("平台[%s]查询到%d个自动续期号码", platformCode, len(allPhones)) + return allPhones, nil +} diff --git a/app/admin/service/sms_abnormal_number_test.go b/app/admin/service/sms_abnormal_number_test.go new file mode 100644 index 0000000..c180105 --- /dev/null +++ b/app/admin/service/sms_abnormal_number_test.go @@ -0,0 +1,20 @@ +package service + +import ( + "testing" + + "github.com/go-admin-team/go-admin-core/logger" +) + +// 同步差异号码 +func TestSyncSat(t *testing.T) { + db := initSetting() + + smsAbnormalNumber := SmsAbnormalNumber{} + smsAbnormalNumber.Orm = db + smsAbnormalNumber.Log = logger.NewHelper(logger.DefaultLogger) + + if err := smsAbnormalNumber.SyncState(); err != nil { + t.Errorf("同步差异号码失败 %v", err) + } +} diff --git a/app/admin/service/sms_daisysms.go b/app/admin/service/sms_daisysms.go index c1a742f..e92e9e8 100644 --- a/app/admin/service/sms_daisysms.go +++ b/app/admin/service/sms_daisysms.go @@ -25,7 +25,9 @@ type SmsDaisysms struct { // 同步价格 func (e SmsDaisysms) SyncPrices() { - prices, err := e.GetPrices() + smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log) + apiInfo, err := smsPlatformKeyRedis.GetRoundRobinKey(global.SmsPlatformDaisysms) + prices, err := e.GetPrices(apiInfo) if err != nil { e.Log.Errorf("GetPrices error: %v", err) @@ -44,16 +46,12 @@ func (e SmsDaisysms) SyncPrices() { // service 服务code // maxPrice 最大价格 // period 时长(月) -func (e *SmsDaisysms) GetNumberForApi(getType int, serviceCode string, maxPrice decimal.Decimal, period int) (int, string, int) { +func (e *SmsDaisysms) GetNumberForApi(apiInfo *dto.SmsPlatformKeyQueueDto, getType int, serviceCode string, maxPrice decimal.Decimal, period int) (int, string, int) { acitvationId := 0 result := "" resultCode := statuscode.Success - configResp, code := GetApiKey(e) - if code != statuscode.Success { - return acitvationId, result, code - } - url := fmt.Sprintf("?api_key=%s&action=getNumber&service=%s", configResp.ConfigValue, serviceCode) + url := fmt.Sprintf("?api_key=%s&action=getNumber&service=%s", apiInfo.ApiKey, serviceCode) if getType == 1 { url = fmt.Sprintf("%s&duration=%dM", url, period) @@ -103,13 +101,8 @@ func (e *SmsDaisysms) GetNumberForApi(getType int, serviceCode string, maxPrice } // 设置租赁结束 -func (e *SmsDaisysms) setStatus(id int) int { - configResp, code := GetApiKey(e) - if code != statuscode.Success { - return code - } - - url := fmt.Sprintf("?api_key=%s&action=setStatus&id=%d&status=6", configResp.ConfigValue, id) +func (e *SmsDaisysms) setStatus(id int, apiInfo *dto.SmsPlatformKeyQueueDto) int { + url := fmt.Sprintf("?api_key=%s&action=setStatus&id=%d&status=6", apiInfo.ApiKey, id) client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) bytes, _, err := client.GetRaw(url, nil) @@ -127,34 +120,14 @@ func (e *SmsDaisysms) setStatus(id int) int { } } -func GetApiKey(e *SmsDaisysms) (dto.GetSysConfigByKEYForServiceResp, int) { - configService := SysConfig{Service: e.Service} - configResp := dto.GetSysConfigByKEYForServiceResp{} - err := configService.GetWithKey(&dto.SysConfigByKeyReq{ConfigKey: "sms_key"}, &configResp) - - if err != nil { - e.Log.Errorf("获取短信api失败, %s", err) - return dto.GetSysConfigByKEYForServiceResp{}, statuscode.ServerError - } - - if configResp.ConfigValue == "" { - e.Log.Error("短信api不能为空") - return dto.GetSysConfigByKEYForServiceResp{}, statuscode.ServerError - } - return configResp, statuscode.Success -} - // GetCodeForApi 获取验证码 // messageId 短信id // return 验证码, 状态码 -func (e *SmsDaisysms) GetCodeForApi(messageId string) (string, int) { +func (e *SmsDaisysms) GetCodeForApi(messageId string, apiInfo *dto.SmsPlatformKeyQueueDto) (string, int) { result := "" - key, code := GetApiKey(e) + code := statuscode.Success - if code != statuscode.Success { - return result, code - } - url := fmt.Sprintf("?api_key=%s&action=getStatus&id=%s", key.ConfigValue, messageId) + url := fmt.Sprintf("?api_key=%s&action=getStatus&id=%s", apiInfo.ApiKey, messageId) client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) bytes, _, err := client.GetRaw(url, nil) @@ -183,13 +156,10 @@ func (e *SmsDaisysms) GetCodeForApi(messageId string) (string, int) { // getExtraActivation 获取额外的激活 // messageId 短信id // return 验证码, 状态码 -func (e *SmsDaisysms) getExtraActivation(activationId string) (int, int) { +func (e *SmsDaisysms) getExtraActivation(activationId string, apiInfo *dto.SmsPlatformKeyQueueDto) (int, int) { result := 0 - key, err := GetApiKey(e) - if err != statuscode.Success { - return 0, statuscode.ServerError - } - url := fmt.Sprintf("?api_key=%s&action=getExtraActivation&activationId=%s", key.ConfigValue, activationId) + + url := fmt.Sprintf("?api_key=%s&action=getExtraActivation&activationId=%s", apiInfo.ApiKey, activationId) client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) bytes, _, err1 := client.GetRaw(url, nil) @@ -224,14 +194,9 @@ func (e *SmsDaisysms) getExtraActivation(activationId string) (int, int) { } // KeepLongTerm 长期租赁 -func (e *SmsDaisysms) KeepLongTerm(activationId string) int { - key, code := GetApiKey(e) +func (e *SmsDaisysms) KeepLongTerm(activationId string, apiInfo *dto.SmsPlatformKeyQueueDto) int { - if code != statuscode.Success { - return statuscode.ServerError - } - - url := fmt.Sprintf("?api_key=%s&action=keep&id=%s", key.ConfigValue, activationId) + url := fmt.Sprintf("?api_key=%s&action=keep&id=%s", apiInfo.ApiKey, activationId) client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) bytes, _, err := client.GetRaw(url, nil) @@ -250,15 +215,8 @@ func (e *SmsDaisysms) KeepLongTerm(activationId string) int { } // 取消租赁 -func (e *SmsDaisysms) CancelRental(activationId string) int { - key, code := GetApiKey(e) - - if code != statuscode.Success { - e.Log.Errorf("租赁api请求失败 %s") - return statuscode.ServerError - } - - url := fmt.Sprintf("?api_key=%s&action=setStatus&id=%s&status=8", key.ConfigValue, activationId) +func (e *SmsDaisysms) CancelRental(activationId string, apiInfo *dto.SmsPlatformKeyQueueDto) int { + url := fmt.Sprintf("?api_key=%s&action=setStatus&id=%s&status=8", apiInfo.ApiKey, activationId) client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) bytes, _, err := client.GetRaw(url, nil) @@ -277,16 +235,10 @@ func (e *SmsDaisysms) CancelRental(activationId string) int { } // 获取价格 -func (e *SmsDaisysms) GetPrices() ([]dto.DaisysmsPriceResp, error) { +func (e *SmsDaisysms) GetPrices(apiInfo *dto.SmsPlatformKeyQueueDto) ([]dto.DaisysmsPriceResp, error) { result := make([]dto.DaisysmsPriceResp, 0) - key, code := GetApiKey(e) - if code != statuscode.Success { - e.Log.Errorf("租赁api请求失败 %s") - return result, errors.New("获取租赁ApiKey失败") - } - - url := fmt.Sprintf("?api_key=%s&action=getPrices", key.ConfigValue) + url := fmt.Sprintf("?api_key=%s&action=getPrices", apiInfo.ApiKey) client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) bytes, status, err := client.GetRaw(url, nil) @@ -327,15 +279,9 @@ func (e *SmsDaisysms) GetPrices() ([]dto.DaisysmsPriceResp, error) { // ChangeAutoRenew 修改自动续期 // activationId 短信id // status 状态 -func (e *SmsDaisysms) ChangeAutoRenewForApi(activationId string, status bool) int { - key, err := GetApiKey(e) +func (e *SmsDaisysms) ChangeAutoRenewForApi(activationId string, status bool, apiInfo *dto.SmsPlatformKeyQueueDto) int { - if err != statuscode.Success { - e.Log.Errorf("查询sms api请求失败 %d", activationId) - return statuscode.ServerError - } - - url := fmt.Sprintf("?api_key=%s&action=setAutoRenew&id=%s&value=%t", key.ConfigValue, activationId, status) + url := fmt.Sprintf("?api_key=%s&action=setAutoRenew&id=%s&value=%t", apiInfo.ApiKey, activationId, status) client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) bytes, _, err1 := client.GetRaw(url, nil) diff --git a/app/admin/service/sms_phone.go b/app/admin/service/sms_phone.go index 98885d0..f2b08fc 100644 --- a/app/admin/service/sms_phone.go +++ b/app/admin/service/sms_phone.go @@ -2,6 +2,7 @@ package service import ( "errors" + "fmt" "net/http" "strconv" "time" @@ -93,15 +94,22 @@ func (e *SmsPhone) CancelNumber(req *dto.SmsPhoneCancelNumberReq, userId int) in // 聚合取消 func (e *SmsPhone) CancelNumberManage(data *models.SmsPhone) int { + smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log) + apiInfo, err := smsPlatformKeyRedis.GetApiInfo(data.PlatformCode, data.ApiKey) + if err != nil { + e.Log.Errorf("获取平台密钥失败, %s", err) + return statuscode.ServerError + } + switch data.PlatformCode { case global.SmsPlatformDaisysms: service := SmsDaisysms{Service: e.Service} - return service.CancelRental(data.NewActivationId) + return service.CancelRental(data.NewActivationId, &apiInfo) case global.SmsPlatformTextVerified: service := SmsTextVerified{Service: e.Service} - return service.CancelRental(data.NewActivationId, data.Type) + return service.CancelRental(data.NewActivationId, data.Type, &apiInfo) default: return statuscode.SmsPlatformUnavailable } @@ -139,13 +147,21 @@ func (e *SmsPhone) DeleteMyNumber(req *dto.DeleteMyNumberReq, userId int) int { func (e *SmsPhone) WeakUp(req *dto.WeakUpReq, userId int, defult bool) (dto.WeakUpResp, int) { smsPhone := models.SmsPhone{} result := dto.WeakUpResp{} - if err := e.Orm.Model(smsPhone).Where("activation_id =? and user_id= ?", req.ActivationId, userId).First(&smsPhone).Error; err != nil { + if err := e.Orm.Model(smsPhone). + Where("activation_id =? and user_id= ?", req.ActivationId, userId). + First(&smsPhone).Error; err != nil { e.Log.Errorf("获取短信号码失败, %s", err) return result, statuscode.ServerError } if smsPhone.Status != 1 || defult { - newActivationId, messageId, startTime, endTime, code := e.WeakUpManage(&smsPhone) + smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log) + apiInfo, err := smsPlatformKeyRedis.GetApiInfo(smsPhone.PlatformCode, smsPhone.ApiKey) + if err != nil { + e.Log.Errorf("获取平台密钥失败, %s", err) + return result, statuscode.ServerError + } + newActivationId, messageId, startTime, endTime, code := e.WeakUpManage(&smsPhone, &apiInfo) if code == statuscode.ServerError { return result, code @@ -179,15 +195,15 @@ func (e *SmsPhone) WeakUp(req *dto.WeakUpReq, userId int, defult bool) (dto.Weak // 唤醒号码 // returns newActivationId,messageId, code -func (e *SmsPhone) WeakUpManage(req *models.SmsPhone) (string, string, *time.Time, *time.Time, int) { +func (e *SmsPhone) WeakUpManage(req *models.SmsPhone, apiInfo *dto.SmsPlatformKeyQueueDto) (string, string, *time.Time, *time.Time, int) { switch req.PlatformCode { case global.SmsPlatformDaisysms: service := SmsDaisysms{Service: e.Service} - id, code := service.getExtraActivation(req.ActivationId) + id, code := service.getExtraActivation(req.ActivationId, apiInfo) return strconv.Itoa(id), "", nil, nil, code case global.SmsPlatformTextVerified: service := SmsTextVerified{Service: e.Service} - resp, code := service.getExtraActivation(req.NewActivationId) + resp, code := service.getExtraActivation(req.NewActivationId, apiInfo) return resp.ReservationId, resp.Id, resp.UsageWindowStart, resp.UsageWindowEnd, code default: return "", "", nil, nil, statuscode.SmsPlatformUnavailable @@ -300,7 +316,7 @@ func (e *SmsPhone) DoGetNumber(balanceService *MemberBalance, req *dto.GetNumber } now := time.Now() - activationId, phone, code, expireTime, startTime, endTime := e.GetNumberManage(req.PlatformCode, req.Type, req.ServiceCode, price, req.Period) + activationId, phone, code, expireTime, startTime, endTime, apiInfo := e.GetNumberManage(req.PlatformCode, req.Type, req.ServiceCode, price, req.Period) if code != statuscode.Success { return decimal.Decimal{}, models.SmsPhone{}, code @@ -320,6 +336,7 @@ func (e *SmsPhone) DoGetNumber(balanceService *MemberBalance, req *dto.GetNumber smsPhone.Actived = 1 smsPhone.StartTime = startTime smsPhone.EndTime = endTime + smsPhone.ApiKey = apiInfo.ApiKey smsPhone.Price = price if req.Type == 1 { @@ -361,7 +378,7 @@ func (e *SmsPhone) DoGetNumber(balanceService *MemberBalance, req *dto.GetNumber if req.Type == 1 { var code int if req.PlatformCode == global.SmsPlatformDaisysms { - code = e.ChangeAutoRenewManage(smsPhone.PlatformCode, smsPhone.NewActivationId, true) + code = e.ChangeAutoRenewManage(smsPhone.PlatformCode, smsPhone.ApiKey, smsPhone.NewActivationId, true) } else { code = http.StatusOK e.WeakUp(&dto.WeakUpReq{ActivationId: smsPhone.NewActivationId}, userId, true) @@ -386,24 +403,32 @@ func (e *SmsPhone) DoGetNumber(balanceService *MemberBalance, req *dto.GetNumber // *expirateTime 号码过期时间 // *startTime 长效号码单次接码开始使用(textverified有用) // *endTime 长效号码单次接码过期时间(textverified有用) -func (e *SmsPhone) GetNumberManage(platformCode string, typ int, serviceCode string, price decimal.Decimal, period int) (string, string, int, *time.Time, *time.Time, *time.Time) { +func (e *SmsPhone) GetNumberManage(platformCode string, typ int, serviceCode string, + price decimal.Decimal, period int) (string, string, int, *time.Time, *time.Time, *time.Time, *dto.SmsPlatformKeyQueueDto) { + smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log) + queue, err := smsPlatformKeyRedis.GetRoundRobinKey(platformCode) + if err != nil { + e.Log.Errorf("获取短信平台队列失败, %s", err) + return "", "", statuscode.ServerError, nil, nil, nil, queue + } + switch platformCode { case global.SmsPlatformDaisysms: service := SmsDaisysms{Service: e.Service} - activationId, phone, code := service.GetNumberForApi(typ, serviceCode, price, period) + activationId, phone, code := service.GetNumberForApi(queue, typ, serviceCode, price, period) - return strconv.Itoa(activationId), phone, code, nil, nil, nil + return strconv.Itoa(activationId), phone, code, nil, nil, nil, queue case global.SmsPlatformTextVerified: service := SmsTextVerified{Service: e.Service} - resp, code := service.GetNumberAndWakeUp(typ, serviceCode, price, period) + resp, code := service.GetNumberAndWakeUp(typ, serviceCode, price, period, queue) if code != statuscode.Success { - return "", "", code, nil, resp.StartTime, resp.EndTime + return "", "", code, nil, resp.StartTime, resp.EndTime, queue } - return resp.Id, resp.Phone, code, resp.EndAt, resp.StartTime, resp.EndTime + return resp.Id, resp.Phone, code, resp.EndAt, resp.StartTime, resp.EndTime, queue default: - return "", "", statuscode.SmsPlatformUnavailable, nil, nil, nil + return "", "", statuscode.SmsPlatformUnavailable, nil, nil, nil, queue } } @@ -445,10 +470,29 @@ func (e *SmsPhone) SyncCodes() error { if err := e.Orm.Model(models.SmsPhone{}).Where("status =1").Find(&phones).Error; err != nil { return err } + mapData := make(map[string]*dto.SmsPlatformKeyQueueDto) + smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log) for _, item := range phones { + key := fmt.Sprintf("%s:%s", item.PlatformCode, item.ApiKey) + + if _, ok := mapData[key]; !ok { + apiInfo, _ := smsPlatformKeyRedis.GetApiInfo(item.PlatformCode, item.ApiKey) + + if apiInfo.ApiKey != "" { + mapData[key] = &apiInfo + } + } + } + + for _, item := range phones { + apiInfo, ok := mapData[fmt.Sprintf("%s:%s", item.PlatformCode, item.ApiKey)] + if !ok { + continue + } + code, codeStatus := e.GetCodeManage(item.PlatformCode, item.NewActivationId, - item.Type, item.UserId, item.Service, item.ServiceCode) + item.Type, item.UserId, item.Service, item.ServiceCode, apiInfo) if code == "" { var expireTime time.Time @@ -510,15 +554,15 @@ func (e *SmsPhone) SyncCodes() error { } // 获取验证码 -func (e *SmsPhone) GetCodeManage(platformCode string, messageId string, typ int, userId int, smsService, serviceCode string) (string, int) { +func (e *SmsPhone) GetCodeManage(platformCode string, messageId string, typ int, userId int, smsService, serviceCode string, apiInfo *dto.SmsPlatformKeyQueueDto) (string, int) { switch platformCode { case global.SmsPlatformDaisysms: service := SmsDaisysms{Service: e.Service} - return service.GetCodeForApi(messageId) + return service.GetCodeForApi(messageId, apiInfo) case global.SmsPlatformTextVerified: service := SmsTextVerified{Service: e.Service} - return service.GetCode(messageId, typ, userId, smsService, serviceCode) + return service.GetCode(messageId, typ, userId, smsService, serviceCode, apiInfo) default: return "", statuscode.SmsPlatformUnavailable } @@ -685,7 +729,7 @@ func (e *SmsPhone) handleInsufficientBalance(phone models.SmsPhone) error { } // 调用平台取消续费接口 - code := e.ChangeAutoRenewManage(phone.PlatformCode, phone.ActivationId, false) + code := e.ChangeAutoRenewManage(phone.PlatformCode, phone.ApiKey, phone.ActivationId, false) if code != statuscode.Success { params["auto_renewal"] = 1 params["remark"] = "" @@ -778,7 +822,7 @@ func (e *SmsPhone) DoChangeAutoRenewal(data models.SmsPhone, autoRenewal int) (b if data.Actived == 3 { code = http.StatusOK } else { - code = e.ChangeAutoRenewManage(data.PlatformCode, data.ActivationId, status) + code = e.ChangeAutoRenewManage(data.PlatformCode, data.ApiKey, data.ActivationId, status) if code != statuscode.Success { return true, statuscode.ServerError @@ -792,16 +836,24 @@ func (e *SmsPhone) DoChangeAutoRenewal(data models.SmsPhone, autoRenewal int) (b } // ChangeAutoRenewManage 修改自动续期管理 -func (e *SmsPhone) ChangeAutoRenewManage(platform string, activationId string, status bool) int { +func (e *SmsPhone) ChangeAutoRenewManage(platform, apiKey string, activationId string, status bool) int { + smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log) + apiInfo, err := smsPlatformKeyRedis.GetApiInfo(platform, apiKey) + + if err != nil { + e.Log.Errorf("获取平台密钥失败: %v", err) + return statuscode.ServerError + } + switch platform { case global.SmsPlatformDaisysms: service := SmsDaisysms{Service: e.Service} - return service.ChangeAutoRenewForApi(activationId, status) + return service.ChangeAutoRenewForApi(activationId, status, &apiInfo) case global.SmsPlatformTextVerified: service := SmsTextVerified{Service: e.Service} - return service.Renew(activationId, status) + return service.Renew(activationId, status, &apiInfo) default: return statuscode.SmsPlatformUnavailable } diff --git a/app/admin/service/sms_phone_extend.go b/app/admin/service/sms_phone_extend.go new file mode 100644 index 0000000..bb27ba5 --- /dev/null +++ b/app/admin/service/sms_phone_extend.go @@ -0,0 +1 @@ +package service diff --git a/app/admin/service/sms_platform_key.go b/app/admin/service/sms_platform_key.go new file mode 100644 index 0000000..fc1b28a --- /dev/null +++ b/app/admin/service/sms_platform_key.go @@ -0,0 +1,314 @@ +package service + +import ( + "errors" + "fmt" + + "github.com/go-admin-team/go-admin-core/sdk/service" + "gorm.io/gorm" + + "go-admin/app/admin/models" + "go-admin/app/admin/service/dto" + "go-admin/common/actions" + cDto "go-admin/common/dto" + "go-admin/utils/utility" +) + +type SmsPlatformKey struct { + service.Service +} + +// GetPage 获取SmsPlatformKey列表 +func (e *SmsPlatformKey) GetPage(c *dto.SmsPlatformKeyGetPageReq, p *actions.DataPermission, list *[]models.SmsPlatformKey, count *int64) error { + var err error + var data models.SmsPlatformKey + + err = e.Orm.Model(&data). + Scopes( + cDto.MakeCondition(c.GetNeedSearch()), + cDto.Paginate(c.GetPageSize(), c.GetPageIndex()), + actions.Permission(data.TableName(), p), + ). + Find(list).Limit(-1).Offset(-1). + Count(count).Error + if err != nil { + e.Log.Errorf("SmsPlatformKeyService GetPage error:%s \r\n", err) + return err + } + + for index := range *list { + (*list)[index].ApiKey = utility.DesensitizeGeneric((*list)[index].ApiKey, 3, 3, '*') + (*list)[index].ApiSecret = utility.DesensitizeGeneric((*list)[index].ApiSecret, 3, 3, '*') + } + + return nil +} + +// GetRandomKey 随机获取一个密钥 +func (e *SmsPlatformKey) GetRandomKey(platformCode string) (*dto.SmsPlatformKeyQueueDto, error) { + redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log) + return redisService.GetRandomKey(platformCode) +} + +// GetRoundRobinKey 轮询获取密钥 +func (e *SmsPlatformKey) GetRoundRobinKey(platformCode string) (*dto.SmsPlatformKeyQueueDto, error) { + redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log) + return redisService.GetRoundRobinKey(platformCode) +} + +// GetQueueLength 获取队列长度 +func (e *SmsPlatformKey) GetQueueLength(platformCode string) (int64, error) { + redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log) + return redisService.GetQueueLength(platformCode) +} + +// ClearPlatformQueue 清空指定平台的队列 +func (e *SmsPlatformKey) ClearPlatformQueue(platformCode string) error { + redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log) + return redisService.ClearPlatformQueue(platformCode) +} + +// Get 获取SmsPlatformKey对象 +func (e *SmsPlatformKey) Get(d *dto.SmsPlatformKeyGetReq, p *actions.DataPermission, model *models.SmsPlatformKey) error { + var data models.SmsPlatformKey + + err := e.Orm.Model(&data). + Scopes( + actions.Permission(data.TableName(), p), + ). + First(model, d.GetId()).Error + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + err = errors.New("查看对象不存在或无权查看") + e.Log.Errorf("Service GetSmsPlatformKey error:%s \r\n", err) + return err + } + if err != nil { + e.Log.Errorf("db error:%s", err) + return err + } + return nil +} + +// Insert 创建SmsPlatformKey对象 +func (e *SmsPlatformKey) Insert(c *dto.SmsPlatformKeyInsertReq) error { + var err error + var data models.SmsPlatformKey + var count int64 + c.Generate(&data) + + if err := e.Orm.Model(&models.SmsPlatformKey{}). + Where("platform_code = ? and api_key =? and account=?", data.PlatformCode, data.ApiKey, data.Account). + Count(&count).Error; err != nil { + e.Log.Errorf("SmsPlatformKeyService Insert error:%s \r\n", err) + return err + } + if count > 0 { + return errors.New("ApiKey或账号 已存在") + } + + err = e.Orm.Create(&data).Error + if err != nil { + e.Log.Errorf("SmsPlatformKeyService Insert error:%s \r\n", err) + return err + } + + if err := e.AddQueque(dto.SmsPlatformKeyQueueDto{ + PlatformCode: data.PlatformCode, + Account: data.Account, + ApiKey: data.ApiKey, + ApiSecret: data.ApiSecret, + }); err != nil { + e.Log.Errorf("添加队列失败,%v", err) + return err + } + + return nil +} + +// Update 修改SmsPlatformKey对象 +func (e *SmsPlatformKey) Update(c *dto.SmsPlatformKeyUpdateReq, p *actions.DataPermission) error { + var err error + var data = models.SmsPlatformKey{} + var count int64 + e.Orm.Scopes( + actions.Permission(data.TableName(), p), + ).First(&data, c.GetId()) + oldKey := data.ApiKey + oldSecret := data.ApiSecret + oldStatus := data.Status + oldPlatformCode := data.PlatformCode + + if err1 := e.Orm.Model(&models.SmsPlatformKey{}). + Where("platform_code = ? and api_key =? and account=? and id != ?", + data.PlatformCode, data.ApiKey, data.Account, data.Id). + Count(&count).Error; err1 != nil { + e.Log.Errorf("SmsPlatformKeyService Insert error:%s \r\n", err1) + return err1 + } + if count > 0 { + return errors.New("ApiKey或账号 已存在") + } + + c.Generate(&data) + + // 如果要将启用状态改为禁用状态,需要检查该平台是否还有其他启用的密钥 + if data.Status == 2 { + var remainingCount int64 + err1 := e.Orm.Model(&models.SmsPlatformKey{}). + Where("platform_code = ? AND status = ? AND id != ?", oldPlatformCode, 1, c.GetId()). + Count(&remainingCount).Error + if err1 != nil { + e.Log.Errorf("检查平台剩余启用密钥失败: %v", err1) + return errors.New("检查平台剩余启用密钥失败") + } + if remainingCount == 0 { + return fmt.Errorf("平台 %s 至少需要保留一个启用状态的密钥,无法禁用", oldPlatformCode) + } + } + + db := e.Orm.Save(&data) + if err = db.Error; err != nil { + e.Log.Errorf("SmsPlatformKeyService Save error:%s \r\n", err) + return err + } + if db.RowsAffected == 0 { + return errors.New("无权更新该数据") + } + + if oldStatus == 1 && data.Status == 2 { + if err := e.RemoveQueque(dto.SmsPlatformKeyQueueDto{ + PlatformCode: data.PlatformCode, + Account: data.Account, + ApiKey: data.ApiKey, + ApiSecret: data.ApiSecret, + }, false); err != nil { + e.Log.Errorf("删除失败,%v", err) + return err + } + } else if oldStatus == 2 && data.Status == 1 { + if err := e.AddQueque(dto.SmsPlatformKeyQueueDto{ + PlatformCode: data.PlatformCode, + Account: data.Account, + ApiKey: data.ApiKey, + ApiSecret: data.ApiSecret, + }); err != nil { + e.Log.Errorf("添加队列失败,%v", err) + return err + } + } else { + oldQueueData := dto.SmsPlatformKeyQueueDto{ + PlatformCode: data.PlatformCode, + Account: data.Account, + ApiKey: oldKey, + ApiSecret: oldSecret, + } + queueData := dto.SmsPlatformKeyQueueDto{ + PlatformCode: data.PlatformCode, + Account: data.Account, + ApiKey: data.ApiKey, + ApiSecret: data.ApiSecret, + } + if err := e.Replace(oldQueueData, queueData); err != nil { + e.Log.Errorf("替换队列失败,%v", err) + return err + } + } + + return nil +} + +// Remove 删除SmsPlatformKey +func (e *SmsPlatformKey) Remove(d *dto.SmsPlatformKeyDeleteReq, p *actions.DataPermission) error { + var data models.SmsPlatformKey + var datas []models.SmsPlatformKey + + if err1 := e.Orm.Where("id in (?)", d.GetId()).Find(&datas).Error; err1 != nil { + e.Log.Errorf("Service RemoveSmsPlatformKey error:%s \r\n", err1) + return err1 + } + + var platformCodes []string + + for _, item := range datas { + if utility.ContainsString(platformCodes, item.PlatformCode) { + continue + } + + platformCodes = append(platformCodes, item.PlatformCode) + } + + err := e.Orm.Transaction(func(tx *gorm.DB) error { + db := tx.Model(&data). + Scopes( + actions.Permission(data.TableName(), p), + ).Delete(&data, d.GetId()) + + var count []string + + if err1 := tx.Model(&data).Where("status =1"). + Group("platform_code"). + Select("platform_code"). + Scan(&count). + Error; err1 != nil { + e.Log.Errorf("Service RemoveSmsPlatformKey error:%s \r\n", err1) + return err1 + } + + for _, item := range platformCodes { + if !utility.ContainsString(count, item) { + return errors.New("删除失败,通道最少需要保留一个可用ApiKey") + } + } + + if err := db.Error; err != nil { + e.Log.Errorf("Service RemoveSmsPlatformKey error:%s \r\n", err) + return err + } + if db.RowsAffected == 0 { + return errors.New("无权删除该数据") + } + return nil + }) + if err != nil { + return err + } + + for _, item := range datas { + queueDta := dto.SmsPlatformKeyQueueDto{ + PlatformCode: item.PlatformCode, + Account: data.Account, + ApiKey: item.ApiKey, + ApiSecret: item.ApiSecret, + } + + if err := e.RemoveQueque(queueDta, true); err != nil { + e.Log.Errorf("移出队列失败,%v", err) + return errors.New("删除队列失败") + } + } + return nil +} + +// InitQueque 初始化Redis缓存队列 +func (e *SmsPlatformKey) InitQueque() error { + redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log) + return redisService.InitRedisQueque() +} + +// Replace 替换Redis缓存中的密钥 +func (e *SmsPlatformKey) Replace(oldEntity dto.SmsPlatformKeyQueueDto, entity dto.SmsPlatformKeyQueueDto) error { + redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log) + return redisService.ReplaceRedisKey(oldEntity, entity) +} + +// RemoveQueque 从Redis缓存中移出队列 +func (e *SmsPlatformKey) RemoveQueque(entity dto.SmsPlatformKeyQueueDto, shouldDel bool) error { + redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log) + return redisService.RemoveRedisQueque(entity, shouldDel) +} + +// AddQueque 添加到Redis缓存队列 +func (e *SmsPlatformKey) AddQueque(entity dto.SmsPlatformKeyQueueDto) error { + redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log) + return redisService.AddRedisQueque(entity) +} diff --git a/app/admin/service/sms_platform_key_redis.go b/app/admin/service/sms_platform_key_redis.go new file mode 100644 index 0000000..487743a --- /dev/null +++ b/app/admin/service/sms_platform_key_redis.go @@ -0,0 +1,327 @@ +package service + +import ( + "encoding/json" + "errors" + "fmt" + "math/rand" + "time" + + "go-admin/app/admin/models" + "go-admin/app/admin/service/dto" + "go-admin/utils/redishelper" + + "github.com/bytedance/sonic" + "github.com/go-admin-team/go-admin-core/logger" + "github.com/go-admin-team/go-admin-core/sdk/service" + "gorm.io/gorm" +) + +// SmsPlatformKeyRedis Redis版本的SMS平台密钥管理服务 +type SmsPlatformKeyRedis struct { + SmsPlatformKey + redisHelper *redishelper.RedisHelper +} + +// NewSmsPlatformKeyRedis 创建Redis版本的SMS平台密钥服务 +func NewSmsPlatformKeyRedis(orm *gorm.DB, log logger.Logger) *SmsPlatformKeyRedis { + return &SmsPlatformKeyRedis{ + SmsPlatformKey: SmsPlatformKey{ + Service: service.Service{ + Orm: orm, + Log: logger.NewHelper(log), + }, + }, + redisHelper: redishelper.DefaultRedis, + } +} + +// getRedisKey 获取Redis键名 +func (e *SmsPlatformKeyRedis) getRedisKey(platformCode string) string { + return fmt.Sprintf("sms:platform:keys:%s", platformCode) +} + +// getRedisIndexKey 获取Redis索引键名(用于快速查找) +func (e *SmsPlatformKeyRedis) getRedisIndexKey(platformCode, apiKey string) string { + return fmt.Sprintf("sms:platform:index:%s:%s", platformCode, apiKey) +} + +func (e *SmsPlatformKeyRedis) GetApiInfo(platformCode, apiKey string) (dto.SmsPlatformKeyQueueDto, error) { + indexKey := e.getRedisIndexKey(platformCode, apiKey) + data, err := e.redisHelper.GetString(indexKey) + if err != nil { + e.Log.Errorf("获取Redis索引失败 [%s:%s]: %v", platformCode, apiKey, err) + return dto.SmsPlatformKeyQueueDto{}, err + } + + var apiInfo dto.SmsPlatformKeyQueueDto + err = sonic.Unmarshal([]byte(data), &apiInfo) + if err != nil { + e.Log.Errorf("解析Redis数据失败 [%s:%s]: %v", platformCode, apiKey, err) + return dto.SmsPlatformKeyQueueDto{}, err + } + + return apiInfo, nil +} + +// InitRedisQueque 初始化Redis缓存队列 +// 从数据库加载所有SMS平台密钥并存储到Redis中 +func (e *SmsPlatformKeyRedis) InitRedisQueque() error { + // 查询所有启用的SMS平台密钥 + var list []models.SmsPlatformKey + err := e.Orm.Where("status = ?", 1).Find(&list).Error + if err != nil { + e.Log.Errorf("查询SMS平台密钥失败: %v", err) + return err + } + + if len(list) == 0 { + e.Log.Warn("未找到启用的SMS平台密钥") + return nil + } + + // 按平台分组 + platformKeyMap := make(map[string][]dto.SmsPlatformKeyQueueDto) + for _, item := range list { + data := dto.SmsPlatformKeyQueueDto{ + PlatformCode: item.PlatformCode, + Account: item.Account, + ApiKey: item.ApiKey, + ApiSecret: item.ApiSecret, + } + platformKeyMap[item.PlatformCode] = append(platformKeyMap[item.PlatformCode], data) + } + + // 将每个平台的密钥存储到Redis + for platformCode, keys := range platformKeyMap { + redisKey := e.getRedisKey(platformCode) + + // 清空现有数据 + err := e.redisHelper.DeleteString(redisKey) + if err != nil { + e.Log.Errorf("清空Redis队列失败 [%s]: %v", platformCode, err) + } + + // 存储密钥列表 + for _, key := range keys { + keyJson, _ := json.Marshal(key) + err := e.redisHelper.RPushList(redisKey, string(keyJson)) + if err != nil { + e.Log.Errorf("添加密钥到Redis队列失败 [%s]: %v", platformCode, err) + continue + } + + // 创建索引,便于快速查找和删除 + indexKey := e.getRedisIndexKey(platformCode, key.ApiKey) + err = e.redisHelper.SetString(indexKey, string(keyJson)) + if err != nil { + e.Log.Errorf("创建密钥索引失败 [%s:%s]: %v", platformCode, key.ApiKey, err) + } + } + + // 设置过期时间(24小时) + err = e.redisHelper.SetKeyExpiration(redisKey, 24*time.Hour) + if err != nil { + e.Log.Errorf("设置Redis队列过期时间失败 [%s]: %v", platformCode, err) + } + + e.Log.Infof("平台 [%s] 初始化 %d 个密钥到Redis缓存", platformCode, len(keys)) + } + + return nil +} + +// AddRedisQueque 添加密钥到Redis缓存 +func (e *SmsPlatformKeyRedis) AddRedisQueque(entity dto.SmsPlatformKeyQueueDto) error { + redisKey := e.getRedisKey(entity.PlatformCode) + indexKey := e.getRedisIndexKey(entity.PlatformCode, entity.ApiKey) + + // 检查是否已存在 + // exists, err := e.redisHelper.GetString(indexKey) + // if err == nil && exists != "" { + // return errors.New("密钥已存在") + // } + + // 序列化密钥数据 + keyJson, err := json.Marshal(entity) + if err != nil { + return fmt.Errorf("序列化密钥数据失败: %v", err) + } + + // 添加到队列 + err = e.redisHelper.RPushList(redisKey, string(keyJson)) + if err != nil { + return fmt.Errorf("添加密钥到Redis队列失败: %v", err) + } + + // 创建索引 + err = e.redisHelper.SetString(indexKey, string(keyJson)) + if err != nil { + e.Log.Errorf("创建密钥索引失败: %v", err) + // 索引创建失败不影响主要功能 + } + + // 设置过期时间 + err = e.redisHelper.SetKeyExpiration(redisKey, 24*time.Hour) + if err != nil { + e.Log.Errorf("设置Redis队列过期时间失败: %v", err) + } + + e.Log.Infof("成功添加密钥到Redis缓存 [%s:%s]", entity.PlatformCode, entity.ApiKey) + return nil +} + +// RemoveRedisQueque 从Redis缓存中删除密钥 +// shouldDel 是否删除索引 +func (e *SmsPlatformKeyRedis) RemoveRedisQueque(entity dto.SmsPlatformKeyQueueDto, shouldDel bool) error { + redisKey := e.getRedisKey(entity.PlatformCode) + indexKey := e.getRedisIndexKey(entity.PlatformCode, entity.ApiKey) + + // 获取要删除的密钥数据 + keyData, err := e.redisHelper.GetString(indexKey) + if err != nil || keyData == "" { + return errors.New("密钥不存在") + } + + // 从队列中删除 + _, err = e.redisHelper.LRem(redisKey, keyData) + if err != nil { + return fmt.Errorf("从Redis队列删除密钥失败: %v", err) + } + + // 删除索引 + if shouldDel { + err = e.redisHelper.DeleteString(indexKey) + if err != nil { + e.Log.Errorf("删除密钥索引失败: %v", err) + // 索引删除失败不影响主要功能 + } + } + + // e.Log.Infof("成功从Redis缓存删除密钥 [%s:%s]", entity.PlatformCode, entity.ApiKey) + return nil +} + +// GetRandomKey 随机获取一个密钥 +func (e *SmsPlatformKeyRedis) GetRandomKey(platformCode string) (*dto.SmsPlatformKeyQueueDto, error) { + redisKey := e.getRedisKey(platformCode) + + // 获取所有密钥 + keys, err := e.redisHelper.GetAllList(redisKey) + if err != nil { + return nil, fmt.Errorf("获取Redis队列失败: %v", err) + } + + if len(keys) == 0 { + return nil, errors.New("队列为空") + } + + // 随机选择一个 + rand.Seed(time.Now().UnixNano()) + randomIndex := rand.Intn(len(keys)) + keyJson := keys[randomIndex] + + var keyDto dto.SmsPlatformKeyQueueDto + err = json.Unmarshal([]byte(keyJson), &keyDto) + if err != nil { + return nil, fmt.Errorf("反序列化密钥数据失败: %v", err) + } + + return &keyDto, nil +} + +// GetRoundRobinKey 轮询获取密钥 +func (e *SmsPlatformKeyRedis) GetRoundRobinKey(platformCode string) (*dto.SmsPlatformKeyQueueDto, error) { + redisKey := e.getRedisKey(platformCode) + + // 从队列头部取出一个密钥 + keyJson, err := e.redisHelper.LPopList(redisKey) + if err != nil { + return nil, fmt.Errorf("从Redis队列获取密钥失败: %v", err) + } + + if keyJson == "" { + return nil, errors.New("队列为空") + } + + // 将密钥重新放到队列尾部(实现轮询) + err = e.redisHelper.RPushList(redisKey, keyJson) + if err != nil { + e.Log.Errorf("轮询密钥回放失败: %v", err) + } + + var keyDto dto.SmsPlatformKeyQueueDto + err = json.Unmarshal([]byte(keyJson), &keyDto) + if err != nil { + return nil, fmt.Errorf("反序列化密钥数据失败: %v", err) + } + + return &keyDto, nil +} + +// GetQueueLength 获取队列长度 +func (e *SmsPlatformKeyRedis) GetQueueLength(platformCode string) (int64, error) { + redisKey := e.getRedisKey(platformCode) + keys, err := e.redisHelper.GetAllList(redisKey) + if err != nil { + return 0, fmt.Errorf("获取Redis队列长度失败: %v", err) + } + return int64(len(keys)), nil +} + +// ReplaceRedisKey 替换Redis缓存中的密钥 +func (e *SmsPlatformKeyRedis) ReplaceRedisKey(oldEntity, newEntity dto.SmsPlatformKeyQueueDto) error { + // 先删除旧密钥 + err := e.RemoveRedisQueque(oldEntity, true) + if err != nil { + return fmt.Errorf("删除旧密钥失败: %v", err) + } + + // 再添加新密钥 + err = e.AddRedisQueque(newEntity) + if err != nil { + return fmt.Errorf("添加新密钥失败: %v", err) + } + + // e.Log.Infof("成功替换Redis缓存密钥 [%s] %s -> %s", newEntity.PlatformCode, oldEntity.ApiKey, newEntity.ApiKey) + return nil +} + +// ClearPlatformQueue 清空指定平台的队列 +func (e *SmsPlatformKeyRedis) ClearPlatformQueue(platformCode string) error { + redisKey := e.getRedisKey(platformCode) + err := e.redisHelper.DeleteString(redisKey) + if err != nil { + return fmt.Errorf("清空平台队列失败: %v", err) + } + + // 清空相关索引 + pattern := fmt.Sprintf("sms:platform:index:%s:*", platformCode) + err = e.redisHelper.DeleteKeysByPrefix(pattern) + if err != nil { + e.Log.Errorf("清空平台索引失败: %v", err) + } + + e.Log.Infof("成功清空平台 [%s] 的Redis队列", platformCode) + return nil +} + +// 获取平台所有密钥 +func (e *SmsPlatformKeyRedis) GetPlatformKeys(platformCode string) ([]dto.SmsPlatformKeyQueueDto, error) { + redisKey := e.getRedisKey(platformCode) + keys, err := e.redisHelper.GetAllList(redisKey) + if err != nil { + return nil, fmt.Errorf("获取Redis队列失败: %v", err) + } + + var keyDtos []dto.SmsPlatformKeyQueueDto + for _, keyJson := range keys { + var keyDto dto.SmsPlatformKeyQueueDto + err = json.Unmarshal([]byte(keyJson), &keyDto) + if err != nil { + return nil, fmt.Errorf("反序列化密钥数据失败: %v", err) + } + keyDtos = append(keyDtos, keyDto) + } + return keyDtos, nil +} diff --git a/app/admin/service/sms_platform_key_redis_test.go b/app/admin/service/sms_platform_key_redis_test.go new file mode 100644 index 0000000..9363ae6 --- /dev/null +++ b/app/admin/service/sms_platform_key_redis_test.go @@ -0,0 +1,23 @@ +package service + +import ( + "fmt" + "go-admin/common/global" + "testing" + + "github.com/go-admin-team/go-admin-core/logger" +) + +func TestNextKey(t *testing.T) { + orm := initSetting() + + redisService := NewSmsPlatformKeyRedis(orm, logger.DefaultLogger) + + val, err := redisService.GetRoundRobinKey(global.SmsPlatformTextVerified) + + if err != nil { + t.Errorf("GetRoundRobinKey error:%s \r\n", err) + } + + fmt.Printf("val:%s \r\n", val) +} diff --git a/app/admin/service/sms_receive_log.go b/app/admin/service/sms_receive_log.go index 7989184..d0c716d 100644 --- a/app/admin/service/sms_receive_log.go +++ b/app/admin/service/sms_receive_log.go @@ -49,8 +49,16 @@ func (e SmsReceiveLog) WebHook(req *dto.SmsReceiveWebHookReq) error { return err } + smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log) + apiInfo, err := smsPlatformKeyRedis.GetApiInfo(phoneLog.PlatformCode, phoneLog.ApiKey) + if err != nil { + e.Log.Errorf("获取平台密钥失败, %s", err) + return err + } + phoneService := SmsDaisysms{Service: e.Service} - if code := phoneService.setStatus(req.ActivationID); code != statuscode.Success { + + if code := phoneService.setStatus(req.ActivationID, &apiInfo); code != statuscode.Success { e.Log.Errorf("接受验证码回调后修改状态失败 %d", code) } } diff --git a/app/admin/service/sms_receive_log_test.go b/app/admin/service/sms_receive_log_test.go index a8f22a7..4d6b6e6 100644 --- a/app/admin/service/sms_receive_log_test.go +++ b/app/admin/service/sms_receive_log_test.go @@ -13,9 +13,10 @@ import ( ) func initSetting() *gorm.DB { - dsn := "root:123456@tcp(127.0.0.1:3306)/proxy-server-prod?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms" + dsn := "root:123456@tcp(127.0.0.1:3306)/proxy_server?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms" db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{}) sdk.Runtime.SetDb("default", db) + // config.ExtConfig. config.ExtConfig.TrxGridUrl = "https://api.trongrid.io" config.ExtConfig.DaisysmsUrl = "https://daisysms.com/stubs/handler_api.php" config.ExtConfig.SmsTextVerified.ApiKey = "ZQ0swXnsaPpeGdwa3c7gT9U9I1Oh9WoDHx0amuYovvaHuqd5u6B4NBBUSUBjR" diff --git a/app/admin/service/sms_text_verified.go b/app/admin/service/sms_text_verified.go index d1989e8..1c9365b 100644 --- a/app/admin/service/sms_text_verified.go +++ b/app/admin/service/sms_text_verified.go @@ -32,7 +32,7 @@ type SmsTextVerified struct { // 获取收到的验证码 // messageId 短效或长效号码的ID // typ 0-短效 1-长效 -func (e SmsTextVerified) GetCode(messageId string, typ int, userId int, service, serviceCode string) (string, int) { +func (e SmsTextVerified) GetCode(messageId string, typ int, userId int, service, serviceCode string, apiInfo *dto.SmsPlatformKeyQueueDto) (string, int) { reservationType := "" if typ == 0 { @@ -41,7 +41,7 @@ func (e SmsTextVerified) GetCode(messageId string, typ int, userId int, service, reservationType = "renewable" } url := fmt.Sprintf(getSmsCode, messageId, reservationType) - client, code := e.GetTextVerifiedAuthClient() + client, code := e.GetTextVerifiedAuthClient(apiInfo) if code != http.StatusOK { e.Log.Errorf("获取授权请求失败,status %d", code) @@ -168,13 +168,13 @@ var ErrUnAuth = errors.New("未授权,请检查配置的账号和密码") var ErrOutOfStockOrUnavailable = errors.New("缺货或服务不可用") // Login 登录 -func (e *SmsTextVerified) Login() (string, error) { +func (e *SmsTextVerified) Login(apiInfo *dto.SmsPlatformKeyQueueDto) (string, error) { resp := dto.TextVerifiedLoginResp{} var token string client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.SmsTextVerified.Url, nil) headers := map[string]string{ - "X-API-USERNAME": config.ExtConfig.SmsTextVerified.UserName, - "X-API-KEY": config.ExtConfig.SmsTextVerified.ApiKey, + "X-API-USERNAME": apiInfo.Account, + "X-API-KEY": apiInfo.ApiKey, } _, err1 := client.Post(loginUrl, nil, headers, &resp) @@ -192,7 +192,8 @@ func (e *SmsTextVerified) Login() (string, error) { if resp.ExpiresIn >= 10 { resp.ExpiresIn = resp.ExpiresIn - 10 // 提前10秒过期 - if err := redishelper.DefaultRedis.SetStringExpire(global.TextVerifiedToken, token, time.Duration(resp.ExpiresIn)*time.Second); err != nil { + key := fmt.Sprintf(global.TextVerifiedToken, apiInfo.ApiKey) + if err := redishelper.DefaultRedis.SetStringExpire(key, token, time.Duration(resp.ExpiresIn)*time.Second); err != nil { e.Log.Errorf("TextVerified登录失败,缓存Token失败 error: %v", err) } } @@ -201,15 +202,16 @@ func (e *SmsTextVerified) Login() (string, error) { } // 获取token -func (e *SmsTextVerified) GetToken() (string, error) { - token, err := redishelper.DefaultRedis.GetString(global.TextVerifiedToken) +func (e *SmsTextVerified) GetToken(apiInfo *dto.SmsPlatformKeyQueueDto) (string, error) { + key := fmt.Sprintf(global.TextVerifiedToken, apiInfo.ApiKey) + token, err := redishelper.DefaultRedis.GetString(key) if err != nil && errors.Is(err, redis.Nil) { // token不存在,重新登录获取 - return e.Login() + return e.Login(apiInfo) } if token == "" { - return e.Login() + return e.Login(apiInfo) } return token, nil @@ -225,9 +227,9 @@ func (e *SmsTextVerified) GetAreas() ([]string, error) { } // 获取服务列表 -func (e *SmsTextVerified) GetServices() ([]dto.TextVerifiedServeResp, error) { +func (e *SmsTextVerified) GetServices(apiInfo *dto.SmsPlatformKeyQueueDto) ([]dto.TextVerifiedServeResp, error) { client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.SmsTextVerified.Url, nil) - headers, err := e.GetAuthHeader() + headers, err := e.GetAuthHeader(apiInfo) if err != nil { return nil, err } @@ -244,7 +246,15 @@ func (e *SmsTextVerified) GetServices() ([]dto.TextVerifiedServeResp, error) { // 同步服务列表 func (e *SmsTextVerified) SyncServices() error { - services, err := e.GetServices() + // 获取API信息 + smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log) + apiInfo, err := smsPlatformKeyRedis.GetRoundRobinKey(global.SmsPlatformTextVerified) + if err != nil { + e.Log.Errorf("获取API信息失败: %v", err) + return err + } + + services, err := e.GetServices(apiInfo) if err != nil { e.Log.Errorf("获取服务列表失败: %v", err) return err @@ -321,17 +331,25 @@ func (e *SmsTextVerified) SyncPrices() error { return err } + smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log) + apiInfo, err := smsPlatformKeyRedis.GetRoundRobinKey(global.SmsPlatformTextVerified) + + if err != nil { + e.Log.Errorf("获取API信息失败: %v", err) + return err + } + for _, v := range services { params := map[string]interface{}{} - price, code := e.GetPrice(0, v.Code) + price, code := e.GetPrice(0, v.Code, apiInfo) if code == http.StatusOK { params["price"] = price } time.Sleep(time.Microsecond * 500) // 避免请求过快被限制 - longPrice, code := e.GetPrice(1, v.Code) + longPrice, code := e.GetPrice(1, v.Code, apiInfo) if code == http.StatusOK { params["long_price"] = longPrice } @@ -347,14 +365,14 @@ func (e *SmsTextVerified) SyncPrices() error { } // 获取号码并唤醒 -func (e *SmsTextVerified) GetNumberAndWakeUp(tye int, serviceCode string, price decimal.Decimal, period int) (dto.SmsPhoneGetPhoneResp, int) { - resp, code := e.GetNumberForApi(tye, serviceCode, price, period) +func (e *SmsTextVerified) GetNumberAndWakeUp(tye int, serviceCode string, price decimal.Decimal, period int, apiInfo *dto.SmsPlatformKeyQueueDto) (dto.SmsPhoneGetPhoneResp, int) { + resp, code := e.GetNumberForApi(tye, serviceCode, price, period, apiInfo) if code != statuscode.Success { return resp, code } - wakeUpResp, code := e.getExtraActivation(resp.Id) + wakeUpResp, code := e.getExtraActivation(resp.Id, apiInfo) if code != statuscode.Success { return resp, code @@ -372,7 +390,7 @@ func (e *SmsTextVerified) GetNumberAndWakeUp(tye int, serviceCode string, price // service 服务code // maxPrice 最大价格 // period 时长(月=30天) -func (e *SmsTextVerified) GetNumberForApi(typ int, serviceCode string, price decimal.Decimal, period int) (dto.SmsPhoneGetPhoneResp, int) { +func (e *SmsTextVerified) GetNumberForApi(typ int, serviceCode string, price decimal.Decimal, period int, apiInfo *dto.SmsPlatformKeyQueueDto) (dto.SmsPhoneGetPhoneResp, int) { //这个平台的所有号码都是美国号码 默认国家代码都是 +1 var err error var createResp dto.TextVerifiedResp @@ -380,10 +398,10 @@ func (e *SmsTextVerified) GetNumberForApi(typ int, serviceCode string, price dec switch typ { case 0: var code int - createResp, code = e.CreateVerification(serviceCode) + createResp, code = e.CreateVerification(serviceCode, apiInfo) if code == statuscode.Success && createResp.Href != "" { - bytes, err := e.doRequest(&createResp) + bytes, err := e.doRequest(&createResp, apiInfo) if err != nil { e.Log.Errorf("短效号码 获取号码失败:%v", e) @@ -410,11 +428,11 @@ func (e *SmsTextVerified) GetNumberForApi(typ int, serviceCode string, price dec return result, statuscode.ServerError } case 1: - createResp, err = e.CreateRental(serviceCode) + createResp, err = e.CreateRental(serviceCode, apiInfo) if err == nil && createResp.Href != "" { saleId := getIdByUrl(createResp.Href) - bytes, err := e.doRequest(&createResp) + bytes, err := e.doRequest(&createResp, apiInfo) if err != nil { e.Log.Errorf("通过销售id [%s] 获取号码失败:%v", saleId, err) @@ -441,7 +459,7 @@ func (e *SmsTextVerified) GetNumberForApi(typ int, serviceCode string, price dec return result, statuscode.ServerError } - detail, code := e.GetRentalDetail(result.Id) + detail, code := e.GetRentalDetail(result.Id, apiInfo) if code != statuscode.Success { return result, code @@ -477,13 +495,13 @@ func (e *SmsTextVerified) GetNumberForApi(typ int, serviceCode string, price dec } // 执行查询请求 -func (e *SmsTextVerified) doRequest(resp *dto.TextVerifiedResp) ([]byte, error) { +func (e *SmsTextVerified) doRequest(resp *dto.TextVerifiedResp, apiInfo *dto.SmsPlatformKeyQueueDto) ([]byte, error) { bytes := []byte{} if resp.Href == "" { return bytes, errors.New("成功请求没有返回url") } - header, err := e.GetAuthHeader() + header, err := e.GetAuthHeader(apiInfo) if err != nil { return bytes, fmt.Errorf("获取授权失败:%v", err) @@ -501,7 +519,7 @@ func (e *SmsTextVerified) doRequest(resp *dto.TextVerifiedResp) ([]byte, error) } // 长号码租赁 -func (e *SmsTextVerified) CreateRental(serviceCode string) (dto.TextVerifiedResp, error) { +func (e *SmsTextVerified) CreateRental(serviceCode string, apiInfo *dto.SmsPlatformKeyQueueDto) (dto.TextVerifiedResp, error) { req := dto.TextVerifiedCreateRewalReq{ ServiceName: serviceCode, Capability: "sms", @@ -513,7 +531,7 @@ func (e *SmsTextVerified) CreateRental(serviceCode string) (dto.TextVerifiedResp resp := dto.TextVerifiedResp{} - client, code := e.GetTextVerifiedAuthClient() + client, code := e.GetTextVerifiedAuthClient(apiInfo) if code != statuscode.Success { e.Log.Errorf("获取头信息失败 error: %d", code) @@ -541,10 +559,10 @@ func (e *SmsTextVerified) CreateRental(serviceCode string) (dto.TextVerifiedResp } // 获取长效号码详情 -func (e *SmsTextVerified) GetRentalDetail(id string) (dto.VerificationRentalDetailResp, int) { +func (e *SmsTextVerified) GetRentalDetail(id string, apiInfo *dto.SmsPlatformKeyQueueDto) (dto.VerificationRentalDetailResp, int) { result := dto.VerificationRentalDetailResp{} - client, code := e.GetTextVerifiedAuthClient() + client, code := e.GetTextVerifiedAuthClient(apiInfo) if code != statuscode.Success { return result, code } @@ -561,8 +579,8 @@ func (e *SmsTextVerified) GetRentalDetail(id string) (dto.VerificationRentalDeta // 唤醒号码 // returns activationId,messageId,startTime(可用开始时间),endTime(可用结束时间), code, -func (e SmsTextVerified) getExtraActivation(id string) (dto.TextVerifiedWakeUpResp, int) { - client, code := e.GetTextVerifiedAuthClient() +func (e SmsTextVerified) getExtraActivation(id string, apiInfo *dto.SmsPlatformKeyQueueDto) (dto.TextVerifiedWakeUpResp, int) { + client, code := e.GetTextVerifiedAuthClient(apiInfo) result := dto.TextVerifiedWakeUpResp{} if code != statuscode.Success { @@ -584,7 +602,7 @@ func (e SmsTextVerified) getExtraActivation(id string) (dto.TextVerifiedWakeUpRe return result, statuscode.ServerError } else if status == http.StatusCreated || status == http.StatusOK { if resp.Method != "" && resp.Href != "" { - bytes, err := e.doRequest(&resp) + bytes, err := e.doRequest(&resp, apiInfo) if err != nil { e.Log.Errorf("唤醒号码失败 id:%s error: %v", id, err) @@ -615,19 +633,19 @@ func (e SmsTextVerified) getExtraActivation(id string) (dto.TextVerifiedWakeUpRe // 获取价格 // getType 0-短效 1-长效 // returns decimal.Decimal(单价), int(状态code) -func (e *SmsTextVerified) GetPrice(typ int, serviceName string) (decimal.Decimal, int) { +func (e *SmsTextVerified) GetPrice(typ int, serviceName string, apiInfo *dto.SmsPlatformKeyQueueDto) (decimal.Decimal, int) { switch typ { case 1: - return e.GetRentalPrice(serviceName) + return e.GetRentalPrice(serviceName, apiInfo) case 0: - return e.GetVerificationPrice(serviceName) + return e.GetVerificationPrice(serviceName, apiInfo) default: return decimal.Zero, statuscode.SmsInvalidType } } // 获取长租价格 -func (e *SmsTextVerified) GetRentalPrice(serviceName string) (decimal.Decimal, int) { +func (e *SmsTextVerified) GetRentalPrice(serviceName string, apiInfo *dto.SmsPlatformKeyQueueDto) (decimal.Decimal, int) { req := dto.TextVerifiedPriceReq{ ServiceName: serviceName, Capability: "sms", @@ -636,7 +654,7 @@ func (e *SmsTextVerified) GetRentalPrice(serviceName string) (decimal.Decimal, i Duration: "thirtyDay", } - client, code := e.GetTextVerifiedAuthClient() + client, code := e.GetTextVerifiedAuthClient(apiInfo) if code != statuscode.Success { e.Log.Errorf("获取授权请求失败,status %d", code) @@ -659,7 +677,7 @@ func (e *SmsTextVerified) GetRentalPrice(serviceName string) (decimal.Decimal, i } // 获取单次验证码价格 -func (e *SmsTextVerified) GetVerificationPrice(sericeName string) (decimal.Decimal, int) { +func (e *SmsTextVerified) GetVerificationPrice(sericeName string, apiInfo *dto.SmsPlatformKeyQueueDto) (decimal.Decimal, int) { params := map[string]interface{}{ "serviceName": sericeName, "areaCode": false, @@ -668,7 +686,7 @@ func (e *SmsTextVerified) GetVerificationPrice(sericeName string) (decimal.Decim "numberType": "mobile", } - client, code := e.GetTextVerifiedAuthClient() + client, code := e.GetTextVerifiedAuthClient(apiInfo) if code != statuscode.Success { e.Log.Errorf("获取授权请求失败,status %d", code) @@ -690,14 +708,14 @@ func (e *SmsTextVerified) GetVerificationPrice(sericeName string) (decimal.Decim } // 单次接收 -func (e *SmsTextVerified) CreateVerification(serviceCode string) (dto.TextVerifiedResp, int) { +func (e *SmsTextVerified) CreateVerification(serviceCode string, apiInfo *dto.SmsPlatformKeyQueueDto) (dto.TextVerifiedResp, int) { req := dto.TextVerifiedCreateRewalReq{ ServiceName: serviceCode, Capability: "sms", } resp := dto.TextVerifiedResp{} - client, code := e.GetTextVerifiedAuthClient() + client, code := e.GetTextVerifiedAuthClient(apiInfo) if code != statuscode.Success { return resp, code @@ -722,8 +740,8 @@ func (e *SmsTextVerified) CreateVerification(serviceCode string) (dto.TextVerifi // 取消验证码 // typ 0-短效 1-长效 -func (e SmsTextVerified) CancelRental(id string, typ int) int { - client, code := e.GetTextVerifiedAuthClient() +func (e SmsTextVerified) CancelRental(id string, typ int, apiInfo *dto.SmsPlatformKeyQueueDto) int { + client, code := e.GetTextVerifiedAuthClient(apiInfo) if code != statuscode.Success { return code @@ -758,8 +776,8 @@ func (e SmsTextVerified) CancelRental(id string, typ int) int { return statuscode.Success } -func (e *SmsTextVerified) GetTextVerifiedAuthClient() (*httphelper.HTTPClient, int) { - header, err := e.GetAuthHeader() +func (e *SmsTextVerified) GetTextVerifiedAuthClient(apiInfo *dto.SmsPlatformKeyQueueDto) (*httphelper.HTTPClient, int) { + header, err := e.GetAuthHeader(apiInfo) if err != nil { e.Log.Errorf("取消验证码获取token失败 error: %v", err) @@ -882,10 +900,10 @@ func (e *SmsTextVerified) TextVerifiedWebHook(req *dto.TextVerifiedWebHookReq) e // 续期 // typ 0-短效 1-长效 -func (e *SmsTextVerified) Renew(activationId string, status bool) int { +func (e *SmsTextVerified) Renew(activationId string, status bool, apiInfo *dto.SmsPlatformKeyQueueDto) int { url := fmt.Sprintf(updateRentalRenewStatus, activationId) - client, code := e.GetTextVerifiedAuthClient() + client, code := e.GetTextVerifiedAuthClient(apiInfo) if code != statuscode.Success { e.Log.Errorf("获取长效续期失败 %d", code) @@ -916,18 +934,13 @@ func (e *SmsTextVerified) Renew(activationId string, status bool) int { // 重新初始化短信记录 func (e *SmsTextVerified) InitSmsLogs() error { - // phones, err := e.GetNumbers() - - // if err != nil { - // e.Log.Errorf("获取长效号码列表失败 %v", err) - // return fmt.Errorf("获取长效号码列表失败 %v", err) - // } - - // numbers := make([]string, 0) - - // for _, v := range phones { - // numbers = append(numbers, v.Number) - // } + // 获取平台密钥信息 + smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log) + apiInfo, err := smsPlatformKeyRedis.GetApiInfo("textverified", "") + if err != nil { + e.Log.Errorf("获取平台密钥失败: %v", err) + return fmt.Errorf("获取平台密钥失败: %v", err) + } phoneLogs, err := e.GetUserByNumber("textverified") @@ -938,7 +951,7 @@ func (e *SmsTextVerified) InitSmsLogs() error { if len(phoneLogs) > 0 { for _, v := range phoneLogs { - e.GetCode(v.ActivationId, v.Type, v.UserId, v.Service, v.ServiceCode) + e.GetCode(v.ActivationId, v.Type, v.UserId, v.Service, v.ServiceCode, &apiInfo) } } @@ -956,9 +969,48 @@ func (e *SmsTextVerified) GetUserByNumber(platformCode string) ([]models.SmsPhon return result, nil } +// 获取平台下 所有账号的获取记录 +// map[string][]dto.VerificationDTO key:account +func (e *SmsTextVerified) GetAllPlatformNumbers() (map[string][]dto.VerificationDTO, error) { + // 获取平台密钥信息 + smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log) + platformKeys, err := smsPlatformKeyRedis.GetPlatformKeys(global.SmsPlatformTextVerified) + + if err != nil { + e.Log.Errorf("获取平台密钥失败: %v", err) + return nil, fmt.Errorf("获取平台密钥失败: %v", err) + } + + allNumbers := make(map[string][]dto.VerificationDTO) + + for _, v := range platformKeys { + numbersw, err1 := e.GetAllNumbers(&v) + + if err1 != nil { + e.Log.Errorf("获取平台密钥失败: %v", err1) + continue + } + + allNumbers[v.Account] = numbersw + } + + return allNumbers, nil +} + +func (e *SmsTextVerified) GetAllNumbers(apiInfo *dto.SmsPlatformKeyQueueDto) ([]dto.VerificationDTO, error) { + phones, err := e.GetNumbers(apiInfo) + + if err != nil { + e.Log.Errorf("获取长效号码列表失败 %v", err) + return phones, fmt.Errorf("获取长效号码列表失败 %v", err) + } + + return phones, nil +} + // 获取号码 -func (e *SmsTextVerified) GetNumbers() ([]dto.VerificationDTO, error) { - client, code := e.GetTextVerifiedAuthClient() +func (e *SmsTextVerified) GetNumbers(apiInfo *dto.SmsPlatformKeyQueueDto) ([]dto.VerificationDTO, error) { + client, code := e.GetTextVerifiedAuthClient(apiInfo) result := make([]dto.VerificationDTO, 0) if code != statuscode.Success { @@ -967,7 +1019,7 @@ func (e *SmsTextVerified) GetNumbers() ([]dto.VerificationDTO, error) { } resp := dto.TextVerifiedGetNumbersResp{} - statusCode, err := client.Get(getNumbers, map[string]string{}, resp) + statusCode, err := client.Get(getNumbers, map[string]string{}, &resp) if err != nil { e.Log.Errorf("获取长效号码列表失败 %v", err) @@ -976,11 +1028,15 @@ func (e *SmsTextVerified) GetNumbers() ([]dto.VerificationDTO, error) { if statusCode == http.StatusOK { // 添加第一页数据 - result = append(result, resp.Data...) + for _, item := range resp.Data { + item.Number = "1" + item.Number + + result = append(result, item) + } // 处理分页数据 if resp.HasNext { - paginatedData, err := e.fetchPaginatedData(resp.Links.Next) + paginatedData, err := e.fetchPaginatedData(resp.Links.Next, apiInfo) if err != nil { return result, err } @@ -989,7 +1045,7 @@ func (e *SmsTextVerified) GetNumbers() ([]dto.VerificationDTO, error) { return result, nil } else if statusCode == http.StatusUnauthorized { - return e.GetNumbers() + return e.GetNumbers(apiInfo) } else { e.Log.Errorf("获取长效号码列表失败 %d", statusCode) return result, fmt.Errorf("获取长效号码列表失败 %d", statusCode) @@ -999,11 +1055,11 @@ func (e *SmsTextVerified) GetNumbers() ([]dto.VerificationDTO, error) { // 获取授权header头 // fetchPaginatedData 获取分页数据的通用方法 // 处理TextVerified API的分页响应,自动获取所有页面的数据 -func (e *SmsTextVerified) fetchPaginatedData(nextLink *dto.TextVerifiedResp) ([]dto.VerificationDTO, error) { +func (e *SmsTextVerified) fetchPaginatedData(nextLink *dto.TextVerifiedResp, apiInfo *dto.SmsPlatformKeyQueueDto) ([]dto.VerificationDTO, error) { var result []dto.VerificationDTO for nextLink != nil { - nextData, err := e.doRequest(nextLink) + nextData, err := e.doRequest(nextLink, apiInfo) if err != nil { e.Log.Errorf("获取分页数据失败: %v", err) return result, fmt.Errorf("获取分页数据失败: %v", err) @@ -1016,7 +1072,10 @@ func (e *SmsTextVerified) fetchPaginatedData(nextLink *dto.TextVerifiedResp) ([] return result, fmt.Errorf("解析分页数据失败: %v", err) } - result = append(result, nextResp.Data...) + for _, item := range nextResp.Data { + item.Number = "1" + item.Number + result = append(result, item) + } // 检查是否还有下一页 if nextResp.HasNext { @@ -1029,8 +1088,8 @@ func (e *SmsTextVerified) fetchPaginatedData(nextLink *dto.TextVerifiedResp) ([] return result, nil } -func (e *SmsTextVerified) GetAuthHeader() (map[string]string, error) { - token, err := e.GetToken() +func (e *SmsTextVerified) GetAuthHeader(apiInfo *dto.SmsPlatformKeyQueueDto) (map[string]string, error) { + token, err := e.GetToken(apiInfo) if err != nil { return nil, err } diff --git a/app/admin/service/sms_text_verified_enhanced.go b/app/admin/service/sms_text_verified_enhanced.go new file mode 100644 index 0000000..cebfcf8 --- /dev/null +++ b/app/admin/service/sms_text_verified_enhanced.go @@ -0,0 +1,224 @@ +package service + +import ( + "errors" + "fmt" + "go-admin/app/admin/service/dto" + "go-admin/common/global" + "go-admin/common/statuscode" + "go-admin/config" + "go-admin/utils/httphelper" + "go-admin/utils/redishelper" + "net/http" + "strings" + "time" + + "github.com/go-admin-team/go-admin-core/sdk/service" + "github.com/go-redis/redis/v8" +) + +// SmsTextVerifiedEnhanced 增强版TextVerified服务,支持token自动刷新 +type SmsTextVerifiedEnhanced struct { + service.Service + maxRetries int // 最大重试次数 +} + +// NewSmsTextVerifiedEnhanced 创建增强版TextVerified服务实例 +func NewSmsTextVerifiedEnhanced() *SmsTextVerifiedEnhanced { + return &SmsTextVerifiedEnhanced{ + maxRetries: 2, // 默认最大重试2次 + } +} + +// AuthenticatedRequest 带自动token刷新的HTTP请求包装器 +// 当遇到401未授权错误时,会自动刷新token并重试请求 +func (e *SmsTextVerifiedEnhanced) AuthenticatedRequest( + apiInfo *dto.SmsPlatformKeyQueueDto, + requestFunc func(client *httphelper.HTTPClient) (int, error), +) (int, error) { + var lastErr error + + // 尝试执行请求,包含重试逻辑 + for attempt := 0; attempt <= e.maxRetries; attempt++ { + // 获取认证客户端 + client, code := e.GetTextVerifiedAuthClient(apiInfo) + if code != statuscode.Success { + e.Log.Errorf("获取授权客户端失败,状态码: %d", code) + return code, fmt.Errorf("获取授权客户端失败,状态码: %d", code) + } + + // 执行请求 + status, err := requestFunc(client) + + // 如果请求成功,直接返回 + if err == nil { + return status, nil + } + + // 检查是否为401未授权错误 + if e.isUnauthorizedError(status, err) { + e.Log.Warnf("检测到token过期或未授权错误 (尝试 %d/%d): %v", attempt+1, e.maxRetries+1, err) + + // 如果不是最后一次尝试,清除token缓存并重试 + if attempt < e.maxRetries { + if clearErr := e.clearTokenCache(apiInfo); clearErr != nil { + e.Log.Errorf("清除token缓存失败: %v", clearErr) + } + e.Log.Infof("正在重试请求... (尝试 %d/%d)", attempt+2, e.maxRetries+1) + continue + } + } + + // 如果不是401错误或已达到最大重试次数,记录错误并退出循环 + lastErr = err + break + } + + // 返回最后一次的错误 + return statuscode.ServerError, fmt.Errorf("请求失败,已重试%d次: %v", e.maxRetries, lastErr) +} + +// isUnauthorizedError 检查错误是否为401未授权错误 +func (e *SmsTextVerifiedEnhanced) isUnauthorizedError(status int, err error) bool { + if status == http.StatusUnauthorized { + return true + } + + if err != nil { + errorMsg := strings.ToLower(err.Error()) + // 检查常见的未授权错误消息 + return strings.Contains(errorMsg, "unauthorized") || + strings.Contains(errorMsg, "401") || + strings.Contains(errorMsg, "invalid token") || + strings.Contains(errorMsg, "token expired") || + strings.Contains(errorMsg, "authentication failed") + } + + return false +} + +// clearTokenCache 清除Redis中的token缓存 +func (e *SmsTextVerifiedEnhanced) clearTokenCache(apiInfo *dto.SmsPlatformKeyQueueDto) error { + key := fmt.Sprintf(global.TextVerifiedToken, apiInfo.ApiKey) + err := redishelper.DefaultRedis.DeleteString(key) + if err != nil { + return fmt.Errorf("删除token缓存失败: %v", err) + } + e.Log.Infof("已清除token缓存: %s", key) + return nil +} + +// GetTextVerifiedAuthClient 获取认证的HTTP客户端 +// 这个方法与原版相同,但可以在这里添加额外的逻辑 +func (e *SmsTextVerifiedEnhanced) GetTextVerifiedAuthClient(apiInfo *dto.SmsPlatformKeyQueueDto) (*httphelper.HTTPClient, int) { + header, err := e.GetAuthHeader(apiInfo) + if err != nil { + e.Log.Errorf("获取授权头失败: %v", err) + return nil, statuscode.ServerError + } + client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.SmsTextVerified.Url, header) + return client, statuscode.Success +} + +// GetAuthHeader 获取认证头 +func (e *SmsTextVerifiedEnhanced) GetAuthHeader(apiInfo *dto.SmsPlatformKeyQueueDto) (map[string]string, error) { + token, err := e.GetToken(apiInfo) + if err != nil { + return nil, err + } + headers := map[string]string{ + "Authorization": "Bearer " + token, + } + return headers, nil +} + +// GetToken 获取token(与原版相同的逻辑) +func (e *SmsTextVerifiedEnhanced) GetToken(apiInfo *dto.SmsPlatformKeyQueueDto) (string, error) { + key := fmt.Sprintf(global.TextVerifiedToken, apiInfo.ApiKey) + token, err := redishelper.DefaultRedis.GetString(key) + if err != nil && errors.Is(err, redis.Nil) { + // token不存在,重新登录获取 + return e.Login(apiInfo) + } + + if token == "" { + return e.Login(apiInfo) + } + + return token, nil +} + +// Login 登录获取token(与原版相同的逻辑) +func (e *SmsTextVerifiedEnhanced) Login(apiInfo *dto.SmsPlatformKeyQueueDto) (string, error) { + resp := dto.TextVerifiedLoginResp{} + var token string + client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.SmsTextVerified.Url, nil) + headers := map[string]string{ + "X-API-USERNAME": apiInfo.ApiSecret, + "X-API-KEY": apiInfo.ApiKey, + } + _, err1 := client.Post("/api/pub/v2/auth", nil, headers, &resp) + + if err1 != nil { + e.Log.Errorf("TextVerified登录失败 error: %v", err1) + return "", err1 + } + + if resp.Token == "" { + e.Log.Errorf("TextVerified登录失败,返回的Token为空") + return "", errors.New("TextVerified登录失败,返回的Token为空") + } + + token = resp.Token + + if resp.ExpiresIn >= 10 { + resp.ExpiresIn = resp.ExpiresIn - 10 // 提前10秒过期 + key := fmt.Sprintf(global.TextVerifiedToken, apiInfo.ApiKey) + if err := redishelper.DefaultRedis.SetStringExpire(key, token, time.Duration(resp.ExpiresIn)*time.Second); err != nil { + e.Log.Errorf("TextVerified登录失败,缓存Token失败 error: %v", err) + } + } + + return token, nil +} + +// 使用示例:增强版的GetCode方法 +func (e *SmsTextVerifiedEnhanced) GetCodeEnhanced(messageId string, typ int, userId int, service, serviceCode string, apiInfo *dto.SmsPlatformKeyQueueDto) (string, int) { + reservationType := "" + if typ == 0 { + reservationType = "verification" + } else { + reservationType = "renewable" + } + url := fmt.Sprintf("/api/pub/v2/sms?reservationId=%s&reservationType=%s", messageId, reservationType) + + var parsedCode string + var finalStatus int + + // 使用增强的请求方法 + status, err := e.AuthenticatedRequest(apiInfo, func(client *httphelper.HTTPClient) (int, error) { + resp := dto.TextVerifiedSmsResp{} + status, err := client.Get(url, nil, &resp) + + if err != nil { + return status, err + } + + // 处理响应数据... + if len(resp.Data) > 0 { + // 这里可以添加原有的验证码处理逻辑 + parsedCode = resp.Data[0].ParsedCode + } + + return http.StatusOK, nil + }) + + if err != nil { + e.Log.Errorf("获取验证码失败: %v", err) + finalStatus = statuscode.ServerError + } else { + finalStatus = status + } + + return parsedCode, finalStatus +} \ No newline at end of file diff --git a/app/admin/service/sms_text_verified_test.go b/app/admin/service/sms_text_verified_test.go index 9caf354..ac5d98f 100644 --- a/app/admin/service/sms_text_verified_test.go +++ b/app/admin/service/sms_text_verified_test.go @@ -1,6 +1,7 @@ package service import ( + "go-admin/common/global" "testing" "github.com/go-admin-team/go-admin-core/logger" @@ -13,7 +14,15 @@ func TestSmsTextVerifiedLogin(t *testing.T) { s.Orm = db s.Log = logger.NewHelper(logger.DefaultLogger) - token, err := s.Login() + smsPlatformKeyRedis := NewSmsPlatformKeyRedis(s.Orm, s.Log) + apiInfo, err := smsPlatformKeyRedis.GetRoundRobinKey(global.SmsPlatformTextVerified) + + if err != nil { + s.Log.Errorf("获取API信息失败: %v", err) + t.Error("获取API信息失败", err) + } + + token, err := s.Login(apiInfo) if err != nil { t.Errorf("Login failed: %v", err) } else { @@ -27,9 +36,16 @@ func TestSmsTextVerifiedGetServices(t *testing.T) { s := SmsTextVerified{} s.Orm = db s.Log = logger.NewHelper(logger.DefaultLogger) + smsPlatformKeyRedis := NewSmsPlatformKeyRedis(s.Orm, s.Log) + apiInfo, err := smsPlatformKeyRedis.GetRoundRobinKey(global.SmsPlatformTextVerified) + + if err != nil { + s.Log.Errorf("获取API信息失败: %v", err) + t.Error("获取API信息失败", err) + } // Now, test GetServices with the valid token - servicesResp, err := s.GetServices() + servicesResp, err := s.GetServices(apiInfo) if err != nil { t.Errorf("GetServices failed: %v", err) } else { diff --git a/cmd/api/server.go b/cmd/api/server.go index b45edb5..7b0417e 100644 --- a/cmd/api/server.go +++ b/cmd/api/server.go @@ -223,4 +223,12 @@ func initBusinesses() { memberApiService.Log = cliProxyService.Log memberApiService.InitApis() + + // 创建SMS平台密钥服务实例并初始化Redis缓存 + smsPlatformKey := service.SmsPlatformKey{} + smsPlatformKey.Orm = cliProxyService.Orm + smsPlatformKey.Log = cliProxyService.Log + + // 初始化Redis缓存队列 + smsPlatformKey.InitQueque() } diff --git a/cmd/proxy-server b/cmd/proxy-server deleted file mode 100644 index 3d86bd7..0000000 Binary files a/cmd/proxy-server and /dev/null differ diff --git a/common/global/text_verified.go b/common/global/text_verified.go index 981bdef..d0aa716 100644 --- a/common/global/text_verified.go +++ b/common/global/text_verified.go @@ -1,6 +1,6 @@ package global const ( - // TextVerified Token - TextVerifiedToken = "TextVerifiedToken" + // TextVerified Token {apiKey} + TextVerifiedToken = "TextVerifiedToken:%s" ) diff --git a/common/rediskey/sms-platform-key.go b/common/rediskey/sms-platform-key.go new file mode 100644 index 0000000..fcb3cf0 --- /dev/null +++ b/common/rediskey/sms-platform-key.go @@ -0,0 +1,6 @@ +package rediskey + +const ( + //平台key + SmsPlatformKey = "sms_platform_key:%s" +) diff --git a/utils/utility/crypto_helper.go b/utils/utility/crypto_helper.go new file mode 100644 index 0000000..0bce390 --- /dev/null +++ b/utils/utility/crypto_helper.go @@ -0,0 +1,221 @@ +package utility + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "io" +) + +// 加密key +var CryptoKey = "ProxyServer@#(123321)!Keycrypto" + +// CryptoHelper 加密帮助类 +type CryptoHelper struct { + key []byte +} + +// NewCryptoHelper 创建新的加密帮助实例 +// key: 32字节的加密密钥,如果长度不足会自动填充,超出会截断 +func NewCryptoHelper(key string) *CryptoHelper { + // 确保密钥长度为32字节(AES-256) + keyBytes := make([]byte, 32) + copy(keyBytes, []byte(key)) + return &CryptoHelper{ + key: keyBytes, + } +} + +// Encrypt 加密字符串 +// plaintext: 要加密的明文 +// 返回: base64编码的密文和错误信息 +func (c *CryptoHelper) Encrypt(plaintext string) (string, error) { + if plaintext == "" { + return "", errors.New("plaintext cannot be empty") + } + + // 创建AES cipher + block, err := aes.NewCipher(c.key) + if err != nil { + return "", fmt.Errorf("failed to create cipher: %w", err) + } + + // 使用GCM模式 + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("failed to create GCM: %w", err) + } + + // 生成随机nonce + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", fmt.Errorf("failed to generate nonce: %w", err) + } + + // 加密数据 + ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil) + + // 返回base64编码的结果 + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// Decrypt 解密字符串 +// ciphertext: base64编码的密文 +// 返回: 解密后的明文和错误信息 +func (c *CryptoHelper) Decrypt(ciphertext string) (string, error) { + if ciphertext == "" { + return "", errors.New("ciphertext cannot be empty") + } + + // base64解码 + data, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", fmt.Errorf("failed to decode base64: %w", err) + } + + // 创建AES cipher + block, err := aes.NewCipher(c.key) + if err != nil { + return "", fmt.Errorf("failed to create cipher: %w", err) + } + + // 使用GCM模式 + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("failed to create GCM: %w", err) + } + + // 检查数据长度 + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return "", errors.New("ciphertext too short") + } + + // 提取nonce和密文 + nonce, cipherData := data[:nonceSize], data[nonceSize:] + + // 解密数据 + plaintext, err := gcm.Open(nil, nonce, cipherData, nil) + if err != nil { + return "", fmt.Errorf("failed to decrypt: %w", err) + } + + return string(plaintext), nil +} + +// EncryptBytes 加密字节数组 +// data: 要加密的字节数组 +// 返回: 加密后的字节数组和错误信息 +func (c *CryptoHelper) EncryptBytes(data []byte) ([]byte, error) { + if len(data) == 0 { + return nil, errors.New("data cannot be empty") + } + + // 创建AES cipher + block, err := aes.NewCipher(c.key) + if err != nil { + return nil, fmt.Errorf("failed to create cipher: %w", err) + } + + // 使用GCM模式 + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("failed to create GCM: %w", err) + } + + // 生成随机nonce + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, fmt.Errorf("failed to generate nonce: %w", err) + } + + // 加密数据 + ciphertext := gcm.Seal(nonce, nonce, data, nil) + + return ciphertext, nil +} + +// DecryptBytes 解密字节数组 +// ciphertext: 加密后的字节数组 +// 返回: 解密后的字节数组和错误信息 +func (c *CryptoHelper) DecryptBytes(ciphertext []byte) ([]byte, error) { + if len(ciphertext) == 0 { + return nil, errors.New("ciphertext cannot be empty") + } + + // 创建AES cipher + block, err := aes.NewCipher(c.key) + if err != nil { + return nil, fmt.Errorf("failed to create cipher: %w", err) + } + + // 使用GCM模式 + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("failed to create GCM: %w", err) + } + + // 检查数据长度 + nonceSize := gcm.NonceSize() + if len(ciphertext) < nonceSize { + return nil, errors.New("ciphertext too short") + } + + // 提取nonce和密文 + nonce, cipherData := ciphertext[:nonceSize], ciphertext[nonceSize:] + + // 解密数据 + plaintext, err := gcm.Open(nil, nonce, cipherData, nil) + if err != nil { + return nil, fmt.Errorf("failed to decrypt: %w", err) + } + + return plaintext, nil +} + +// GenerateKey 生成随机密钥 +// 返回: 32字节的随机密钥字符串 +func GenerateKey() (string, error) { + key := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + return "", fmt.Errorf("failed to generate key: %w", err) + } + return base64.StdEncoding.EncodeToString(key), nil +} + +// QuickEncrypt 快速加密函数(使用默认密钥) +// plaintext: 要加密的明文 +// key: 加密密钥 +// 返回: base64编码的密文和错误信息 +func QuickEncrypt(plaintext, key string) (string, error) { + crypto := NewCryptoHelper(key) + return crypto.Encrypt(plaintext) +} + +// QuickDecrypt2 快速解密函数(使用默认密钥) +// ciphertext: base64编码的密文 +// 返回: 解密后的明文和错误信息 +func QuickDecrypt2(ciphertext string) (string, error) { + crypto := NewCryptoHelper(CryptoKey) + return crypto.Decrypt(ciphertext) +} + +// QuickEncrypt2 快速加密函数(使用默认密钥) +// plaintext: 要加密的明文 +// 返回: base64编码的密文和错误信息 +func QuickEncrypt2(plaintext string) (string, error) { + crypto := NewCryptoHelper(CryptoKey) + return crypto.Encrypt(plaintext) +} + +// QuickDecrypt 快速解密函数(使用默认密钥) +// ciphertext: base64编码的密文 +// key: 解密密钥 +// 返回: 解密后的明文和错误信息 +func QuickDecrypt(ciphertext, key string) (string, error) { + crypto := NewCryptoHelper(key) + return crypto.Decrypt(ciphertext) +} diff --git a/utils/utility/generic_queue.go b/utils/utility/generic_queue.go new file mode 100644 index 0000000..5e77ab7 --- /dev/null +++ b/utils/utility/generic_queue.go @@ -0,0 +1,371 @@ +package utility + +import ( + "sync" + "time" +) + +// GenericQueue 通用循环队列 +// 支持存储任意类型的数据,实现负载均衡和容错 +type GenericQueue[T any] struct { + data []T // 存储数据的数组 + current int // 当前指针位置 + size int // 队列中的元素数量 + capacity int // 队列容量 + mutex sync.RWMutex // 读写锁,保证线程安全 + lastUsed map[int]time.Time // 记录每个位置的最后使用时间 + cooldown time.Duration // 冷却时间,避免频繁使用同一个元素 + comparer func(T, T) bool // 比较函数,用于检查重复元素 +} + +var ( + // QuequeMap 全局队列映射表,用于管理多个命名队列 + // 使用interface{}类型以支持不同泛型类型的队列 + QuequeMap = make(map[string]interface{}) +) + +// NewGenericQueue 创建新的通用循环队列 +// capacity: 队列容量 +// comparer: 比较函数,用于检查重复元素(可选) +// cooldown: 元素使用冷却时间(可选,默认0表示无冷却) +func NewGenericQueue[T any](capacity int, comparer func(T, T) bool, cooldown ...time.Duration) *GenericQueue[T] { + cd := time.Duration(0) + if len(cooldown) > 0 { + cd = cooldown[0] + } + + return &GenericQueue[T]{ + data: make([]T, capacity), + capacity: capacity, + lastUsed: make(map[int]time.Time), + cooldown: cd, + comparer: comparer, + } +} + +// Add 添加元素到队列 +// item: 要添加的元素 +// 返回: 是否添加成功 +func (q *GenericQueue[T]) Add(item T) bool { + q.mutex.Lock() + defer q.mutex.Unlock() + + if q.size >= q.capacity { + return false // 队列已满 + } + + // 如果提供了比较函数,检查是否已存在相同的元素 + if q.comparer != nil { + for i := 0; i < q.size; i++ { + if q.comparer(q.data[i], item) { + return false // 已存在 + } + } + } + + q.data[q.size] = item + q.size++ + return true +} + +// Remove 从队列中移除指定的元素 +// item: 要移除的元素 +// 返回: 是否移除成功 +func (q *GenericQueue[T]) Remove(item T) bool { + q.mutex.Lock() + defer q.mutex.Unlock() + + if q.comparer == nil { + return false // 没有比较函数无法移除 + } + + for i := 0; i < q.size; i++ { + if q.comparer(q.data[i], item) { + // 将后面的元素前移 + for j := i; j < q.size-1; j++ { + q.data[j] = q.data[j+1] + } + q.size-- + + // 调整current指针 + if q.current >= q.size && q.size > 0 { + q.current = 0 + } + + // 清理lastUsed记录 + delete(q.lastUsed, i) + return true + } + } + return false +} + +// GetNext 获取下一个可用的元素(轮询方式) +// 返回: 元素和是否获取成功 +func (q *GenericQueue[T]) GetNext() (T, bool) { + q.mutex.Lock() + defer q.mutex.Unlock() + + var zero T + if q.size == 0 { + return zero, false // 队列为空 + } + + // 如果没有设置冷却时间,直接返回下一个元素 + if q.cooldown == 0 { + item := q.data[q.current] + q.lastUsed[q.current] = time.Now() + q.current = (q.current + 1) % q.size + return item, true + } + + // 寻找可用的元素(考虑冷却时间) + startPos := q.current + for { + lastUsed, exists := q.lastUsed[q.current] + + // 如果元素从未使用过,或者已过冷却时间 + if !exists || time.Since(lastUsed) >= q.cooldown { + item := q.data[q.current] + q.lastUsed[q.current] = time.Now() + q.current = (q.current + 1) % q.size + return item, true + } + + q.current = (q.current + 1) % q.size + + // 如果遍历了一圈都没找到可用的元素 + if q.current == startPos { + // 返回当前元素,忽略冷却时间 + item := q.data[q.current] + q.lastUsed[q.current] = time.Now() + q.current = (q.current + 1) % q.size + return item, true + } + } +} + +// GetRandom 随机获取一个元素 +// 返回: 元素和是否获取成功 +func (q *GenericQueue[T]) GetRandom() (T, bool) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + var zero T + if q.size == 0 { + return zero, false + } + + // 使用当前时间作为随机种子 + index := int(time.Now().UnixNano()) % q.size + item := q.data[index] + q.lastUsed[index] = time.Now() + return item, true +} + +// GetAll 获取所有元素的副本 +// 返回: 元素切片 +func (q *GenericQueue[T]) GetAll() []T { + q.mutex.RLock() + defer q.mutex.RUnlock() + + items := make([]T, q.size) + copy(items, q.data[:q.size]) + return items +} + +// Size 获取队列中的元素数量 +// 返回: 元素数量 +func (q *GenericQueue[T]) Size() int { + q.mutex.RLock() + defer q.mutex.RUnlock() + return q.size +} + +// IsEmpty 检查队列是否为空 +// 返回: 是否为空 +func (q *GenericQueue[T]) IsEmpty() bool { + q.mutex.RLock() + defer q.mutex.RUnlock() + return q.size == 0 +} + +// IsFull 检查队列是否已满 +// 返回: 是否已满 +func (q *GenericQueue[T]) IsFull() bool { + q.mutex.RLock() + defer q.mutex.RUnlock() + return q.size >= q.capacity +} + +// Clear 清空队列 +func (q *GenericQueue[T]) Clear() { + q.mutex.Lock() + defer q.mutex.Unlock() + + q.size = 0 + q.current = 0 + q.lastUsed = make(map[int]time.Time) +} + +// SetCooldown 设置元素使用冷却时间 +// cooldown: 冷却时间 +func (q *GenericQueue[T]) SetCooldown(cooldown time.Duration) { + q.mutex.Lock() + defer q.mutex.Unlock() + q.cooldown = cooldown +} + +// GetUsageInfo 获取元素使用信息 +// 返回: 位置使用时间映射 +func (q *GenericQueue[T]) GetUsageInfo() map[int]time.Time { + q.mutex.RLock() + defer q.mutex.RUnlock() + + usage := make(map[int]time.Time) + for k, v := range q.lastUsed { + usage[k] = v + } + return usage +} + +// BatchAdd 批量添加元素 +// items: 要添加的元素切片 +// 返回: 成功添加的数量 +func (q *GenericQueue[T]) BatchAdd(items []T) int { + count := 0 + for _, item := range items { + if q.Add(item) { + count++ + } + } + return count +} + +// Replace 替换所有元素 +// items: 新的元素切片 +// 返回: 是否替换成功 +func (q *GenericQueue[T]) Replace(items []T) bool { + if len(items) > q.capacity { + return false + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + q.size = 0 + q.current = 0 + q.lastUsed = make(map[int]time.Time) + + for _, item := range items { + q.data[q.size] = item + q.size++ + } + + return true +} + +// ReplaceItem 替换指定的单个元素 +// oldItem: 要被替换的元素 +// newItem: 新的元素 +// 返回: 是否替换成功 +func (q *GenericQueue[T]) ReplaceItem(oldItem, newItem T) bool { + q.mutex.Lock() + defer q.mutex.Unlock() + + if q.comparer == nil { + return false // 没有比较函数无法查找元素 + } + + for i := 0; i < q.size; i++ { + if q.comparer(q.data[i], oldItem) { + q.data[i] = newItem + return true + } + } + return false // 未找到要替换的元素 +} + +// Enqueue 入队操作(队列尾部添加元素) +// item: 要添加的元素 +// 返回: 是否添加成功 +func (q *GenericQueue[T]) Enqueue(item T) bool { + return q.Add(item) +} + +// Dequeue 出队操作(从队列头部移除并返回元素) +// 返回: 元素和是否成功 +func (q *GenericQueue[T]) Dequeue() (T, bool) { + q.mutex.Lock() + defer q.mutex.Unlock() + + var zero T + if q.size == 0 { + return zero, false // 队列为空 + } + + // 获取队列头部元素 + item := q.data[0] + + // 将后面的元素前移 + for i := 0; i < q.size-1; i++ { + q.data[i] = q.data[i+1] + } + q.size-- + + // 调整current指针 + if q.current > 0 { + q.current-- + } + if q.current >= q.size && q.size > 0 { + q.current = 0 + } + + // 重新映射lastUsed(因为索引发生了变化) + newLastUsed := make(map[int]time.Time) + for index, lastTime := range q.lastUsed { + if index > 0 { + newLastUsed[index-1] = lastTime + } + } + q.lastUsed = newLastUsed + + return item, true +} + +// Peek 查看队列头部元素(不移除) +// 返回: 元素和是否成功 +func (q *GenericQueue[T]) Peek() (T, bool) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + var zero T + if q.size == 0 { + return zero, false // 队列为空 + } + + return q.data[0], true +} + +// PeekLast 查看队列尾部元素(不移除) +// 返回: 元素和是否成功 +func (q *GenericQueue[T]) PeekLast() (T, bool) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + var zero T + if q.size == 0 { + return zero, false // 队列为空 + } + + return q.data[q.size-1], true +} + +// ApiKeyInfo API密钥信息结构体 +type ApiKeyInfo struct { + Key string `json:"key"` // API密钥 + Name string `json:"name"` // 密钥名称 + Weight int `json:"weight"` // 权重 + Enabled bool `json:"enabled"` // 是否启用 + Metadata map[string]string `json:"metadata"` // 元数据 +}