From 82d5acc873040a1619e5327fc3c311db64708ed4 Mon Sep 17 00:00:00 2001 From: hucan <951870319@qq.com> Date: Sat, 23 Aug 2025 16:38:04 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E6=96=B0=E6=8E=A5textverified?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/apis/sms_phone.go | 320 +++++++- app/admin/apis/sms_receive_log.go | 69 +- app/admin/apis/sms_services.go | 28 + app/admin/apis/sys_user.go | 58 ++ app/admin/models/member_api.go | 13 + app/admin/models/sms_phone.go | 7 +- app/admin/models/sms_services.go | 14 +- app/admin/router/sms_phone.go | 10 + app/admin/router/sms_receive_log.go | 3 +- app/admin/router/sms_services.go | 5 +- app/admin/router/sys_user.go | 9 +- app/admin/service/cliproxy_server.go | 2 +- app/admin/service/dto/member_api.go | 30 + app/admin/service/dto/sms_phone.go | 58 +- app/admin/service/dto/sms_services.go | 73 +- app/admin/service/dto/text_verified.go | 214 +++++ app/admin/service/member_api.go | 110 +++ app/admin/service/open_common.go | 1 + app/admin/service/sms_daisysms.go | 355 ++++++++ app/admin/service/sms_phone.go | 596 ++++++-------- app/admin/service/sms_phone_test.go | 65 ++ app/admin/service/sms_receive_log.go | 2 +- app/admin/service/sms_receive_log_test.go | 7 +- app/admin/service/sms_services.go | 169 +++- app/admin/service/sms_text_verified.go | 855 ++++++++++++++++++++ app/admin/service/sms_text_verified_test.go | 53 ++ app/admin/service/sys_config.go | 25 + app/admin/service/sys_dict_data.go | 27 + app/admin/service/sys_user.go | 36 +- app/jobs/examples.go | 1 + app/jobs/sms_job.go | 15 + app/jobs/sms_job_test.go | 10 + app/jobs/trx_job.go | 33 +- app/jobs/trx_job_test.go | 4 + cmd/api/server.go | 11 +- common/global/config_key.go | 13 +- common/global/sms_services.go | 7 + common/global/text_verified.go | 6 + common/middleware/auth.go | 125 ++- common/rediskey/member_api.go | 6 + common/statuscode/status_code.go | 68 +- config/extend.go | 18 +- config/settings.yml | 9 +- go.mod | 6 +- utils/httphelper/http_helper.go | 69 +- 45 files changed, 3154 insertions(+), 461 deletions(-) create mode 100644 app/admin/models/member_api.go create mode 100644 app/admin/service/dto/member_api.go create mode 100644 app/admin/service/dto/text_verified.go create mode 100644 app/admin/service/member_api.go create mode 100644 app/admin/service/open_common.go create mode 100644 app/admin/service/sms_daisysms.go create mode 100644 app/admin/service/sms_phone_test.go create mode 100644 app/admin/service/sms_text_verified.go create mode 100644 app/admin/service/sms_text_verified_test.go create mode 100644 common/global/sms_services.go create mode 100644 common/global/text_verified.go create mode 100644 common/rediskey/member_api.go diff --git a/app/admin/apis/sms_phone.go b/app/admin/apis/sms_phone.go index d606ec9..82d65d1 100644 --- a/app/admin/apis/sms_phone.go +++ b/app/admin/apis/sms_phone.go @@ -13,6 +13,7 @@ import ( "go-admin/app/admin/service" "go-admin/app/admin/service/dto" "go-admin/common/actions" + "go-admin/common/middleware" "go-admin/common/statuscode" ) @@ -194,7 +195,16 @@ func (e SmsPhone) Delete(c *gin.Context) { e.OK(req.GetId(), "删除成功") } -// 租赁号码 +// GetNumber +// @Summary 获取号码 +// @Description 获取号码 +// @Tags 短信手机号 +// @Accept application/json +// @Product application/json +// @Param data body dto.GetNumberReq true "data" +// @Success 200 {object} response.Response{data=decimal.Decimal} "获取号码" +// @Router /api/v1/sms-phone/get-number [post] +// @Security Bearer func (e SmsPhone) GetNumber(c *gin.Context) { req := dto.GetNumberReq{} s := service.SmsPhone{} @@ -211,36 +221,38 @@ func (e SmsPhone) GetNumber(c *gin.Context) { return } - if err := req.Validate(); err != nil { - e.Logger.Errorf("租赁号码失败,%s", err.Error()) + if err1 := req.Validate(); err1 != nil { + e.Logger.Errorf("租赁号码失败,%s", err1.Error()) e.Error(500, err, "服务器错误请联系管理员") return } - var balance decimal.Decimal code := statuscode.Success + lang := "zh" userId := user.GetUserId(c) - smsService, err := servicesService.GetByCode(req.ServiceCode) + smsService, err := servicesService.GetByCode(req.PlatformCode, req.ServiceCode) if err != nil { code = statuscode.SmsServiceUnavailable } else if smsService.Status == 2 { code = statuscode.SmsServiceUnavailable - } else { + } + + var balance decimal.Decimal + if code == statuscode.Success { balance, code = s.GetNumber(&req, userId) } if code != statuscode.Success { if code == statuscode.SmsServiceUnavailable { - e.Error(code, nil, statuscode.GetMsg(code, "zh", smsService.Name)) + e.Error(code, nil, statuscode.GetMsg(code, lang, smsService.Name)) } else { - e.Error(code, nil, statuscode.GetMsg(code, "zh")) + e.Error(code, nil, statuscode.GetMsg(code, lang)) } - return } - e.OK(balance, "租赁号码成功") + e.OK(balance, "获取号码成功") } // 获取验证码 @@ -327,7 +339,7 @@ func (e SmsPhone) WeakUp(c *gin.Context) { } userId := user.GetUserId(c) - code := s.WeakUp(&req, userId) + _, code := s.WeakUp(&req, userId, false) if code != statuscode.Success { e.Error(code, nil, statuscode.GetMsg(code, "zh")) @@ -449,3 +461,289 @@ func (e SmsPhone) CleanMyPhone(c *gin.Context) { e.OK(nil, "清理成功") } + +// OpenGetNumber +// @Summary 开放接口-获取号码 +// @Description 开放接口-获取号码 +// @Tags 短信手机号 +// @Accept application/json +// @Product application/json +// @Param data body dto.GetNumberReq true "data" +// @Success 200 {object} response.Response{data=decimal.Decimal} "获取号码" +// @Router /api/v1/open/sms-phone/get-number [post] +func (e SmsPhone) OpenGetNumber(c *gin.Context) { + req := dto.GetNumberReq{} + s := service.SmsPhone{} + servicesService := service.SmsServices{} + err := e.MakeContext(c). + MakeOrm(). + Bind(&req). + MakeService(&s.Service). + MakeService(&servicesService.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + + if err1 := req.Validate(); err1 != nil { + e.Logger.Errorf("租赁号码失败,%s", err1.Error()) + e.Error(500, err, "服务器错误请联系管理员") + return + } + + lang := "zh" + code := statuscode.Success + userId, err := middleware.GetUserIdByApiKey(c) + + if err != nil { + var errCode int + + switch err { + case middleware.ErrApiUnActived: + errCode = statuscode.ApiUnActived + case middleware.ErrNoAccount: + errCode = statuscode.AccountNotFound + default: + e.Logger.Errorf("获取API用户id失败 %v", err) + } + + e.Error(errCode, nil, statuscode.GetMsg(errCode, lang)) + return + } + + smsService, err := servicesService.GetByCode(req.PlatformCode, req.ServiceCode) + + if err != nil { + code = statuscode.SmsServiceUnavailable + } else if smsService.Status == 2 { + code = statuscode.SmsServiceUnavailable + } + + var resp dto.OpenGetNumberResp + if code == statuscode.Success { + resp, code = s.OpenGetNumber(&req, userId) + } + + if code != statuscode.Success { + if code == statuscode.SmsServiceUnavailable { + e.Error(code, nil, statuscode.GetMsg(code, lang, smsService.Name)) + } else { + e.Error(code, nil, statuscode.GetMsg(code, lang)) + } + return + } + + e.OK(resp, "获取号码成功") +} + +// OpenGetCodeByActivationId 开放api-获取验证码 +// @Summary 开放接口-获取验证码 +// @Description 开放接口-获取验证码 +// @Tags 短信号码 +// @Accept application/json +// @Product application/json +// @Param data body dto.GetCodeReq true "body" +// @Success 200 {object} response.Response{data=dto.GetCodeResp} "成功" +// @Router /api/v1/open/sms-phone/get-code [post] +func (e SmsPhone) OpenGetCodeByActivationId(c *gin.Context) { + req := dto.GetCodeReq{} + s := service.SmsPhone{} + 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 + } + + lang := "zh" + userId, err := middleware.GetUserIdByApiKey(c) + + if err != nil { + var errCode int + + switch err { + case middleware.ErrApiUnActived: + errCode = statuscode.ApiUnActived + case middleware.ErrNoAccount: + errCode = statuscode.AccountNotFound + default: + e.Logger.Errorf("获取API用户id失败 %v", err) + } + + e.Error(errCode, nil, statuscode.GetMsg(errCode, lang)) + return + } + + codeData, code := s.GetCodeByActivationId(&req, userId) + + if code != statuscode.Success { + e.Error(code, nil, statuscode.GetMsg(code, lang)) + return + } + + e.OK(codeData, "获取验证码成功") +} + +// OpenCancel 开放api-取消号码 +// @Summary 开放接口-取消号码 +// @Description 开放接口-取消号码 +// @Tags 短信号码 +// @Accept application/json +// @Product application/json +// @Param data body dto.SmsPhoneCancelNumberReq true "body" +// @Success 200 {object} response.Response "成功" +// @Router /api/v1/open/sms-phone/cancel [post] +func (e SmsPhone) OpenCancel(c *gin.Context) { + req := dto.SmsPhoneCancelNumberReq{} + s := service.SmsPhone{} + 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 + } + + lang := "zh" + userId, err := middleware.GetUserIdByApiKey(c) + + if err != nil { + var errCode int + + switch err { + case middleware.ErrApiUnActived: + errCode = statuscode.ApiUnActived + case middleware.ErrNoAccount: + errCode = statuscode.AccountNotFound + default: + e.Logger.Errorf("获取API用户id失败 %v", err) + } + + e.Error(errCode, nil, statuscode.GetMsg(errCode, lang)) + return + } + + code := s.CancelNumber(&req, userId) + + if code != statuscode.Success { + e.Error(code, nil, statuscode.GetMsg(code, lang)) + return + } + + e.OK(nil, "取消号码成功") +} + +// OpenAutoRenewal 开放api-修改续费状态 +// @Summary 开放接口-修改自动续费状态 +// @Description 开放接口-修改自动续费状态 +// @Tags 短信号码 +// @Accept application/json +// @Product application/json +// @Param data body dto.SmsPhoneChangeAutoRenewReq true "body" +// @Success 200 {object} response.Response "成功" +// @Router /api/v1/open/sms-phone/auto-renewal [post] +func (e SmsPhone) OpenAutoRenewal(c *gin.Context) { + req := dto.SmsPhoneChangeAutoRenewReq{} + s := service.SmsPhone{} + 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 + } + + lang := "zh" + userId, err := middleware.GetUserIdByApiKey(c) + + if err != nil { + var errCode int + + switch err { + case middleware.ErrApiUnActived: + errCode = statuscode.ApiUnActived + case middleware.ErrNoAccount: + errCode = statuscode.AccountNotFound + default: + e.Logger.Errorf("获取API用户id失败 %v", err) + } + + e.Error(errCode, nil, statuscode.GetMsg(errCode, lang)) + return + } + + code := s.ChangeAutoRenew(&req, userId) + + if code != statuscode.Success { + e.Error(code, nil, statuscode.GetMsg(code, lang)) + return + } + + e.OK(nil, "修改自动续费状态成功") +} + +// OpenApi 获取服务列表 +func (e SmsPhone) OpenGetServices(c *gin.Context) { + s := service.SmsServices{} + err := e.MakeContext(c). + MakeOrm(). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + + resp := []dto.SmsServicesGetListResp{} + err = s.GetList(&resp) + + if err != nil { + e.Logger.Error(err) + e.Error(500, err, statuscode.GetMsg(statuscode.ServerError, "en")) + return + } + + e.OK(resp, "success") +} + +// 开放接口-唤醒号码 +func (e SmsPhone) OpenWeakUp(c *gin.Context) { + req := dto.WeakUpReq{} + s := service.SmsPhone{} + + 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 + } + + lang := "en" + userId, err := middleware.GetUserIdByApiKey(c) + + resp, code := s.WeakUp(&req, userId, false) + + if code != statuscode.Success { + e.Error(code, nil, statuscode.GetMsg(code, lang)) + return + } + + e.OK(resp, "success") +} diff --git a/app/admin/apis/sms_receive_log.go b/app/admin/apis/sms_receive_log.go index 47988fd..0734fa8 100644 --- a/app/admin/apis/sms_receive_log.go +++ b/app/admin/apis/sms_receive_log.go @@ -1,7 +1,14 @@ package apis import ( + "bytes" + "crypto/hmac" + "crypto/sha512" + "encoding/base64" "fmt" + "io" + "net/http" + "strings" "github.com/gin-gonic/gin" "github.com/go-admin-team/go-admin-core/sdk/api" @@ -193,7 +200,7 @@ func (e SmsReceiveLog) Delete(c *gin.Context) { e.OK(req.GetId(), "删除成功") } -// WebHook 接收短信发送记录回调 +// daisysms WebHook 接收短信发送记录回调 func (e SmsReceiveLog) WebHook(c *gin.Context) { req := dto.SmsReceiveWebHookReq{} s := service.SmsReceiveLog{} @@ -217,3 +224,63 @@ func (e SmsReceiveLog) WebHook(c *gin.Context) { e.OK(nil, "接收短信发送记录回调成功") } + +// textVerified WebHook 接收短信发送记录回调 +func (e SmsReceiveLog) TextVerifiedWebHook(c *gin.Context) { + // 1. 提前读取原始 body + bodyBytes, err := io.ReadAll(c.Request.Body) + if err != nil { + e.Logger.Error("Failed to read body:", err) + e.Error(500, err, "Failed to read request body") + return + } + + // 2. 验签 + signatureHeader := c.GetHeader("X-Webhook-Signature") + if !verifyWebhookSignature(signatureHeader, bodyBytes, "whsec_UMjFrzkK4YrlOb0AyOFDZAJJ7VWChOVOhYAGKE0e5oKw") { + e.Logger.Warn("Invalid webhook signature") + e.Error(401, fmt.Errorf("invalid signature"), "Invalid signature") + return + } + + // 3. 重置 body 给 Bind 用 + c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + req := dto.TextVerifiedWebHookReq{} + s := service.SmsTextVerified{} + + 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 + } + + err = s.TextVerifiedWebHook(&req) + if err != nil { + c.JSON(500, gin.H{}) + return + } + + c.Status(http.StatusOK) +} + +// 验签方法 +func verifyWebhookSignature(header string, body []byte, secret string) bool { + const prefix = "HMAC-SHA512=" + if !strings.HasPrefix(header, prefix) { + return false + } + sigBase64 := strings.TrimPrefix(header, prefix) + + mac := hmac.New(sha512.New, []byte(secret)) + mac.Write(body) + expectedMAC := mac.Sum(nil) + expectedBase64 := base64.StdEncoding.EncodeToString(expectedMAC) + + return hmac.Equal([]byte(sigBase64), []byte(expectedBase64)) +} diff --git a/app/admin/apis/sms_services.go b/app/admin/apis/sms_services.go index cfce3b8..095b2ef 100644 --- a/app/admin/apis/sms_services.go +++ b/app/admin/apis/sms_services.go @@ -226,6 +226,12 @@ func (e SmsServices) GetPrice(c *gin.Context) { e.Error(500, err, err.Error()) return } + + if req.Valid() != nil { + e.Error(500, req.Valid(), req.Valid().Error()) + return + } + price, err := s.GetPrice(&req) if err != nil { e.Logger.Errorf("获取价格失败,\r\n失败信息 %s", err.Error()) @@ -234,3 +240,25 @@ func (e SmsServices) GetPrice(c *gin.Context) { } e.OK(price, "获取价格成功") } + +// 获取翻译平台 +func (e SmsServices) GetPlatform(c *gin.Context) { + s := service.SmsServices{} + err := e.MakeContext(c). + MakeOrm(). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + + list := make([]dto.SmsPlatformGetListResp, 0) + err = s.GetPlatform(&list) + if err != nil { + e.Logger.Errorf("获取翻译平台失败,\r\n失败信息 %s", err.Error()) + } + + e.OK(list, "获取翻译平台成功") +} diff --git a/app/admin/apis/sys_user.go b/app/admin/apis/sys_user.go index 913727e..08ed39e 100644 --- a/app/admin/apis/sys_user.go +++ b/app/admin/apis/sys_user.go @@ -492,3 +492,61 @@ func (e SysUser) GetInfo(c *gin.Context) { mp["code"] = 200 e.OK(mp, "") } + +func (e SysUser) GetApiInfo(c *gin.Context) { + s := service.MemberApi{} + err := e.MakeContext(c). + MakeOrm(). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + + userId := user.GetUserId(c) + memberData := dto.MemberApiResp{} + err = s.GetByUserId(userId, &memberData) + + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + + e.OK(memberData, "") +} + +// 修改Api状态 +func (e SysUser) ChangeApiStatus(c *gin.Context) { + req := dto.MemberApiChangeStatusReq{} + s := service.MemberApi{} + err := e.MakeContext(c). + MakeOrm(). + MakeService(&s.Service). + Bind(&req). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + + if err := req.Validate(); err != nil { + e.Error(500, err, err.Error()) + return + } + + userId := user.GetUserId(c) + err = s.ChangeStatus(req.Id, userId, req.Status) + + if err != nil { + e.Logger.Errorf("修改api状态失败") + + e.Error(500, err, err.Error()) + return + } + + e.OK(nil, "") +} diff --git a/app/admin/models/member_api.go b/app/admin/models/member_api.go new file mode 100644 index 0000000..b73b6d7 --- /dev/null +++ b/app/admin/models/member_api.go @@ -0,0 +1,13 @@ +package models + +import "go-admin/common/models" + +type MemberApi struct { + models.Model + + UserId int `json:"userId" gorm:"type:bigint;comment:用户id"` + ApiKey string `json:"api" gorm:"type:varchar(255);comment:api_key"` + Status int `json:"status" gorm:"type:tinyint;comment:状态 1-启用 2-禁用"` + models.ModelTime + models.ControlBy +} diff --git a/app/admin/models/sms_phone.go b/app/admin/models/sms_phone.go index a4bc886..9d13c94 100644 --- a/app/admin/models/sms_phone.go +++ b/app/admin/models/sms_phone.go @@ -10,15 +10,16 @@ import ( type SmsPhone struct { models.Model + PlatformCode string `json:"platformCode" gorm:"type:varchar(20);comment:平台code"` UserId int `json:"userId" gorm:"type:bigint;comment:用户Id"` 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-长效"` Period int `json:"period" gorm:"type:int;comment:时长(月)"` Phone string `json:"phone" gorm:"type:varchar(30);comment:号码"` - ActivationId int `json:"activationId" gorm:"type:int;comment:激活码id"` - NewActivationId int `json:"newActivationId" gorm:"type:int;comment:新激活码id 每次获取验证码会刷新"` - MessageId int `json:"messageId" gorm:"type:int;comment:短信模板id"` + ActivationId string `json:"activationId" gorm:"type:varchar(50);comment:激活码id"` + NewActivationId string `json:"newActivationId" gorm:"type:varchar(50);comment:新激活码id 每次获取验证码会刷新"` + MessageId string `json:"messageId" gorm:"type:varchar(50);comment:短信模板id"` Code string `json:"code" gorm:"type:varchar(10);comment:验证码"` Status int `json:"status" gorm:"type:tinyint;comment:状态 1-等待验证码 2-已获取"` ExpireTime *time.Time `json:"expireTime" gorm:"type:datetime;comment:过期时间"` diff --git a/app/admin/models/sms_services.go b/app/admin/models/sms_services.go index 2ad0a8a..bc61eb8 100644 --- a/app/admin/models/sms_services.go +++ b/app/admin/models/sms_services.go @@ -2,15 +2,21 @@ package models import ( "go-admin/common/models" + + "github.com/shopspring/decimal" ) type SmsServices struct { models.Model - Name string `json:"name" gorm:"type:varchar(255);comment:服务名称"` - Code string `json:"code" gorm:"type:varchar(50);comment:编码"` - ExpirationMinutes int `json:"expirationMinutes" gorm:"type:int;comment:过期时间(分钟)"` - Status int `json:"status" gorm:"type:tinyint;comment:状态 1-启用 2-禁用"` + PlatformCode string `json:"platformCode" gorm:"type:varchar(20);comment:平台编码"` + Name string `json:"name" gorm:"type:varchar(255);comment:服务名称"` + Code string `json:"code" gorm:"type:varchar(50);comment:编码"` + ExpirationMinutes int `json:"expirationMinutes" gorm:"type:int;comment:过期时间(分钟)"` + Status int `json:"status" gorm:"type:tinyint;comment:状态 1-启用 2-禁用"` + Price decimal.Decimal `json:"price" gorm:"type:decimal(10,2);comment:价格"` + LongPrice decimal.Decimal `json:"longPrice" gorm:"type:decimal(10,2);comment:长号码价格"` + Icon string `json:"icon" gorm:"type:varchar(255);comment:图标"` models.ModelTime models.ControlBy } diff --git a/app/admin/router/sms_phone.go b/app/admin/router/sms_phone.go index 8e2ee14..eccf602 100644 --- a/app/admin/router/sms_phone.go +++ b/app/admin/router/sms_phone.go @@ -29,6 +29,16 @@ func registerSmsPhoneRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddl r1.PUT("auto-renewal", api.ChangeAutoRenew) //修改自动续费状态 } + r2 := v1.Group("/sms").Use(middleware.FrontedAuth) + { + r2.POST("getNumber", api.OpenGetNumber) // 开放接口 租赁号码 + r2.GET("getCodeByActivationId", api.OpenGetCodeByActivationId) //开放接口-获取验证码 + r2.PUT("cancel", api.OpenCancel) //开放接口-取消号码 + r2.PUT("auto-renewal", api.OpenAutoRenewal) //开放接口-修改自动续费 + r2.GET("services", api.OpenGetServices) //开放接口-获取服务列表 + r2.POST("weak-up", api.OpenWeakUp) //开放接口-唤醒号码 + } + r := v1.Group("/sms-phone").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole()) { r.GET("", actions.PermissionAction(), api.GetPage) diff --git a/app/admin/router/sms_receive_log.go b/app/admin/router/sms_receive_log.go index 1980de2..45d6d4a 100644 --- a/app/admin/router/sms_receive_log.go +++ b/app/admin/router/sms_receive_log.go @@ -27,6 +27,7 @@ func registerSmsReceiveLogRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWT r1 := v1.Group("sms-receive-log") { - r1.POST("webhook", api.WebHook) //接收短信发送记录回调 + r1.POST("webhook", api.WebHook) //接收短信发送记录回调 + r1.POST("text-verified-webhook", api.TextVerifiedWebHook) //接收TextVerified短信发送记录回调 } } diff --git a/app/admin/router/sms_services.go b/app/admin/router/sms_services.go index 08ff5fd..ff37df0 100644 --- a/app/admin/router/sms_services.go +++ b/app/admin/router/sms_services.go @@ -27,7 +27,8 @@ func registerSmsServicesRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMi r1 := v1.Group("/sms-services").Use(authMiddleware.MiddlewareFunc()) { - r1.GET("list", api.GetList) //获取服务列表 - r1.GET("price", api.GetPrice) //获取服务价格 + r1.GET("/platform", api.GetPlatform) //获取平台列表 + r1.GET("/list", api.GetList) //获取服务列表 + r1.GET("/price", api.GetPrice) //获取服务价格 } } diff --git a/app/admin/router/sys_user.go b/app/admin/router/sys_user.go index 4a545a6..bb73fe5 100644 --- a/app/admin/router/sys_user.go +++ b/app/admin/router/sys_user.go @@ -1,11 +1,12 @@ 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" + + "github.com/gin-gonic/gin" + jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" ) func init() { @@ -35,5 +36,7 @@ func registerSysUserRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddle v1auth := v1.Group("").Use(authMiddleware.MiddlewareFunc()) { v1auth.GET("/getinfo", api.GetInfo) + v1auth.GET("/api-info", api.GetApiInfo) + v1auth.PUT("/api-info", api.ChangeApiStatus) } -} \ No newline at end of file +} diff --git a/app/admin/service/cliproxy_server.go b/app/admin/service/cliproxy_server.go index faddf0c..169f29d 100644 --- a/app/admin/service/cliproxy_server.go +++ b/app/admin/service/cliproxy_server.go @@ -44,7 +44,7 @@ func (e *CliProxyService) GetTrafficInfo() ([]dto.CliProxyTraffics, error) { client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.CliproxyUrl, headers) api := "/json/traffic_coutry.json" - err := client.Get(api, nil, &resp) + _, err := client.Get(api, nil, &resp) if err != nil { return nil, err diff --git a/app/admin/service/dto/member_api.go b/app/admin/service/dto/member_api.go new file mode 100644 index 0000000..954b99d --- /dev/null +++ b/app/admin/service/dto/member_api.go @@ -0,0 +1,30 @@ +package dto + +import "errors" + +type MemberApiInsertReq struct { + UserId int `json:"userId" gorm:"type:bigint;comment:用户id"` +} + +type MemberApiResp struct { + Id int `json:"id"` + UserId int `json:"userId" gorm:"type:bigint;comment:用户id"` + ApiKey string `json:"api" gorm:"type:varchar(255);comment:apiKey"` + Status int `json:"status" gorm:"type:tinyint;comment:状态 1-启用 2-禁用"` +} + +type MemberApiChangeStatusReq struct { + Id int `json:"id"` + Status int `json:"status" comment:"1-启用 2-禁用"` +} + +func (e *MemberApiChangeStatusReq) Validate() error { + if e.Id <= 0 { + return errors.New("id不能为空") + } + if e.Status != 1 && e.Status != 2 { + return errors.New("状态不正确") + } + + return nil +} diff --git a/app/admin/service/dto/sms_phone.go b/app/admin/service/dto/sms_phone.go index 7697722..98040e3 100644 --- a/app/admin/service/dto/sms_phone.go +++ b/app/admin/service/dto/sms_phone.go @@ -5,11 +5,15 @@ import ( "go-admin/app/admin/models" "go-admin/common/dto" common "go-admin/common/models" + "time" + + "github.com/shopspring/decimal" ) type SmsPhoneGetPageReq struct { dto.Pagination `search:"-"` Service string `form:"service" search:"type:exact;column:service;table:sms_phone" comment:"sms 服务"` + PlatformCode string `form:"platformCode" search:"type:exact;column:platform_code;table:sms_phone" comment:"平台code"` ServiceCode string `form:"serviceCode" search:"type:exact;column:service_code;table:sms_phone" comment:"服务code"` Type int `form:"type" search:"-" comment:"类型 0-短效 1-长效"` SmsPhoneOrder @@ -109,9 +113,10 @@ func (s *SmsPhoneDeleteReq) GetId() interface{} { } type GetNumberReq struct { - Type int `json:"type" form:"type" comment:"类型 0-短效 1-长效"` - ServiceCode string `json:"serviceCode" form:"serviceCode" comment:"服务code"` - Period int `json:"period" form:"period" comment:"时长(月)"` + PlatformCode string `json:"platformCode" form:"platformCode" commment:"平台code"` + Type int `json:"type" form:"type" comment:"类型 0-短效 1-长效"` + ServiceCode string `json:"serviceCode" form:"serviceCode" comment:"服务code"` + Period int `json:"period" form:"period" comment:"时长(月)"` } func (s *GetNumberReq) Validate() error { @@ -131,21 +136,26 @@ func (s *GetNumberReq) Validate() error { } type GetCodeReq struct { - ActivationIds []int `form:"activationIds"` + ActivationIds []string `form:"activationIds"` } type GetCodeResp struct { - ActivationId int `json:"activationId" comment:"激活码id"` + ActivationId string `json:"activationId" comment:"激活码id"` Code string `json:"code" comment:"验证码"` Status int `json:"status" comment:"状态 1-等待验证码 2-已获取"` } type WeakUpReq struct { - ActivationId int `json:"activationId" comment:"激活码id"` + ActivationId string `json:"activationId" comment:"激活码id"` +} + +type WeakUpResp struct { + ActivationId string `json:"activationId" comment:"激活id"` + MessageId string `json:"messageId" commment:"消息id"` } func (s *WeakUpReq) Validate() error { - if s.ActivationId <= 0 { + if s.ActivationId == "" { return errors.New("激活码id不能为空") } @@ -165,14 +175,42 @@ func (s *DeleteMyNumberReq) Validate() error { } type SmsPhoneCancelNumberReq struct { - Id int `json:"id" comment:"短信号码id"` + Id int `json:"id" comment:"短信号码id"` + ActivationId string `json:"activationId" comment:"短信id"` } type SmsPhoneChangeAutoRenewReq struct { - Id int `json:"id" comment:"短信号码id"` - AutoRenew int `json:"autoRenew" comment:"是否自动续费 1-不自动续费 2-自动续费"` + Id int `json:"id" comment:"短信号码id"` + ActivationId string `json:"activationId" comment:"激活码id id或activationId "` + AutoRenew int `json:"autoRenew" comment:"是否自动续费 1-不自动续费 2-自动续费"` } type SmsPhoneCleanMyPhoneReq struct { Type int `json:"type" comment:"类型 0-短效 1-长效"` } + +type SmsPhoneGetPhoneResp struct { + Phone string `json:"phone"` + EndAt *time.Time `json:"endTime"` + Id string `json:"id"` +} + +type DaisysmsPriceResp struct { + Key string `json:"key"` + Name string `json:"name"` + Count int `json:"count"` + Cost string `json:"cost" comment:"单价"` + Repeatable bool `json:"repeatable"` +} + +type OpenGetNumberResp struct { + Blance decimal.Decimal `json:"balance"` + Number string `json:"number"` + ActivationId string `json:"activationId"` + ExpireTime *time.Time `json:"expireTime"` +} + +type OpenWakeUpReq struct { + PlatformCode string `json:"platformCode" comment:"短信平台"` + ActivationId string `json:"activationId" comment:"平台id"` +} diff --git a/app/admin/service/dto/sms_services.go b/app/admin/service/dto/sms_services.go index f10eec5..a70218e 100644 --- a/app/admin/service/dto/sms_services.go +++ b/app/admin/service/dto/sms_services.go @@ -1,13 +1,19 @@ package dto import ( + "errors" "go-admin/app/admin/models" "go-admin/common/dto" common "go-admin/common/models" + + "github.com/shopspring/decimal" ) type SmsServicesGetPageReq struct { dto.Pagination `search:"-"` + PlatformCode string `form:"platformCode" search:"type:exact;column:platform_code;table:sms_services" comment:"平台编码"` + Name string `form:"name" search:"type:contains;column:name;table:sms_services" comment:"服务名称"` + Status int `form:"status" search:"-"` SmsServicesOrder } @@ -27,9 +33,14 @@ func (m *SmsServicesGetPageReq) GetNeedSearch() interface{} { } type SmsServicesInsertReq struct { - Id int `json:"-" comment:""` // - Name string `json:"name" comment:"服务名称"` - Code string `json:"code" comment:"编码"` + Id int `json:"-" comment:""` // + Name string `json:"name" comment:"服务名称"` + Code string `json:"code" comment:"编码"` + PlatformCode string `json:"platformCode" comment:"平台编码"` + Status int `json:"status" comment:"状态 1-启用 2-禁"` + Price decimal.Decimal `json:"price" comment:"价格"` + LongPrice decimal.Decimal `json:"longPrice" comment:"长号码价格"` + ExpirationMinutes int `json:"expirationMinutes" comment:"过期时间(分钟)"` common.ControlBy } @@ -39,6 +50,11 @@ func (s *SmsServicesInsertReq) Generate(model *models.SmsServices) { } model.Name = s.Name model.Code = s.Code + model.PlatformCode = s.PlatformCode + model.Status = s.Status + model.Price = s.Price + model.LongPrice = s.LongPrice + model.ExpirationMinutes = s.ExpirationMinutes model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的 } @@ -47,9 +63,14 @@ func (s *SmsServicesInsertReq) GetId() interface{} { } type SmsServicesUpdateReq struct { - Id int `uri:"id" comment:""` // - Name string `json:"name" comment:"服务名称"` - Code string `json:"code" comment:"编码"` + Id int `uri:"id" comment:""` // + Name string `json:"name" comment:"服务名称"` + Code string `json:"code" comment:"编码"` + PlatformCode string `json:"platformCode" comment:"平台编码"` + Status int `json:"status" comment:"状态 1-启用 2-禁"` + Price decimal.Decimal `json:"price" comment:"价格"` + LongPrice decimal.Decimal `json:"longPrice" comment:"长号码价格"` + ExpirationMinutes int `json:"expirationMinutes" comment:"过期时间(分钟)"` common.ControlBy } @@ -59,6 +80,11 @@ func (s *SmsServicesUpdateReq) Generate(model *models.SmsServices) { } model.Name = s.Name model.Code = s.Code + model.PlatformCode = s.PlatformCode + model.Status = s.Status + model.Price = s.Price + model.LongPrice = s.LongPrice + model.ExpirationMinutes = s.ExpirationMinutes model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的 } @@ -85,11 +111,38 @@ func (s *SmsServicesDeleteReq) GetId() interface{} { } type SmsServicesGetListResp struct { - Name string `json:"name" comment:"服务名称"` - Code string `json:"code" comment:"编码"` - Status int `json:"status" comment:"状态"` + PlatformCode string `json:"platformCode" comment:"平台编码"` + PlatformName string `json:"platformName" comment:"平台名称"` + Code string `json:"code" comment:"编码"` + Name string `json:"name" comment:"服务名称"` + Status int `json:"status" comment:"状态"` + Price decimal.Decimal `json:"price" comment:"价格"` + LongPrice decimal.Decimal `json:"longPrice" comment:"长号码价格"` } type SmsGetPriceReq struct { - Type int `json:"type" form:"type" comment:"类型"` + PlatformCode string `json:"platformCode" form:"platformCode" comment:"平台code"` + ServiceCode string `json:"serviceCode" form:"serviceCode" comment:"服务code"` + Type int `json:"type" form:"type" comment:"类型"` +} + +func (e *SmsGetPriceReq) Valid() error { + if e.PlatformCode == "" { + return errors.New("平台code不能为空") + } + + if e.ServiceCode == "" { + return errors.New("服务code不能为空") + } + + if e.Type < 0 || e.Type > 1 { + return errors.New("类型错误") + } + + return nil +} + +type SmsPlatformGetListResp struct { + Label string `json:"label" comment:"平台名称"` + Value string `json:"value" comment:"平台code"` } diff --git a/app/admin/service/dto/text_verified.go b/app/admin/service/dto/text_verified.go new file mode 100644 index 0000000..fcff52d --- /dev/null +++ b/app/admin/service/dto/text_verified.go @@ -0,0 +1,214 @@ +package dto + +import ( + "time" + + "github.com/shopspring/decimal" +) + +// 登录相应 +type TextVerifiedLoginResp struct { + Token string `json:"token"` + ExpiresIn int `json:"expiresIn"` + ExpiresAt string `json:"expiresAt"` +} + +// 获取区号响应 +type TextVerifiedAreaResp struct { + AreaCode string `json:"areaCode"` + State string `json:"state"` +} + +// 服务列表 +type TextVerifiedServeResp struct { + ServiceName string `json:"serviceName"` + Capability string `json:"capability"` +} + +// 创建租赁 +type TextVerifiedCreateRewalReq struct { + AllowBackOrderReservations bool `json:"allowBackOrderReservations" comment:"如果设置为 true,当请求的租赁商品缺货时,将创建租赁订单"` + AlwaysOn bool `json:"alwaysOn" comment:"如果设置为 true,则有库存时将分配一条不需要唤醒的线路"` + AreaCodeSelectOption []string `json:"areaCodeSelectOption" comment:"可选的区号数组"` + Duration string `json:"duration" comment:"枚举 oneDay┃threeDay┃sevenDay┃fourteenDay┃thirtyDay┃ninetyDay┃oneYear"` + IsRenewable bool `json:"isRenewable" comment:"是否可再生"` + NumberType string `json:"numberType" comment:"枚举 mobile┃voip┃landline"` + BillingCycleIdToAssignTo string `json:"billingCycleIdToAssignTo"` + ServiceName string `json:"serviceName"` + Capability string `json:"capability" comment:"枚举 sms┃call┃smsAndCall"` +} + +// 回调请求 +type TextVerifiedWebHookReq struct { + Attempt int `json:"attempt" comment:"尝试次数"` + OccurredAt string `json:"occurredAt" comment:"发生时间"` + Event string `json:"event" comment:"事件类型 v2.rental.billingcycle.renewed | v2.rental.billingcycle.expired | v2.sms.received | v2.rental.backorder.fulfilled | v2.reservation.created"` + Id string `json:"id" comment:"唯一标识"` + Data map[string]interface{} `json:"data" comment:"数据"` +} + +// 续费成功回调请求 +type TextVerifiedWebHookRenewedReq struct { + BillingCycleId string `json:"billingCycleId" comment:"续费的计费周期ID"` +} + +// 计费周期过期回调请求 - 当您帐户的计费周期到期时触发。 +type TextVerifiedWebHookBillingCycleExpiredReq struct { + BillingCycleId string `json:"billingCycleId" comment:"过期的计费周期ID"` +} + +// 收到短信回调请求 - 当收到短信并将其分配到您的帐户时触发。 +type TextVerifiedWebHookSmsReceivedReq struct { + From string `json:"from" comment:"发送号码"` + To string `json:"to" comment:"接收号码"` + CreatedAt string `json:"createdAt" comment:"创建时间"` + SmsContent string `json:"smsContent" comment:"短信内容"` + ParsedCode string `json:"parsedCode" comment:"解析后的验证码"` + Encrypted bool `json:"encrypted" comment:"短信内容是否静态加密 "` + ReservationId string `json:"reservationId" comment:"此短信被分配到的预订的 ID。"` +} + +// 订单预定完成时触发 +type TextVerifiedWebHookFulfilledReq struct { + BackOrderId string `json:"backOrderId" comment:"订单预定ID"` +} + +// 预定订单创建回调 +type TextVerifiedWebHookReservationCreatedReq struct { + Id string `json:"id" comment:"订单ID"` + Type string `json:"type" comment:"订单类型 verifications | rentals"` +} + +// 唤醒请求 +type TextVerifiedWakeUpReq struct { + RevervationId string `json:"reservationId"` +} + +// 唤醒响应 +type TextVerifiedWakeUpResp struct { + Id string `json:"id"` + UsageWindowStart *time.Time `json:"usageWindowStart"` + UsageWindowEnd *time.Time `json:"usageWindowEnd"` + IsScheduled bool `json:"isScheduled"` + ReservationId string `json:"reservationId" comment:"预定id"` +} + +// 请求统一响应 +type TextVerifiedResp struct { + Method string `json:"method"` + Href string `json:"href"` +} + +type TextVerifiedCreateRentalReservationDto struct { + ID string `json:"id" comment:"号码id"` + Link TextVerifiedResp `json:"link"` + ReservationType string `json:"reservationType"` + ServiceName string `json:"serviceName"` +} + +// 长效号码响应 +type SaleResponse struct { + CreatedAt time.Time `json:"createdAt"` + ID string `json:"id" comment:"销售id"` + BackOrderReservations []interface{} `json:"backOrderReservations"` // 由于数组为空,使用 interface{} + Reservations []TextVerifiedCreateRentalReservationDto `json:"reservations" comment:"号码信息"` + State string `json:"state"` + Total float64 `json:"total"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// 单次验证码创建后查询 +type VerificationDTO struct { + Number string `json:"number"` + SMS TextVerifiedResp `json:"sms"` + Calls TextVerifiedResp `json:"calls"` + CreatedAt time.Time `json:"createdAt"` + EndsAt time.Time `json:"endsAt"` + ID string `json:"id"` + Cancel Action `json:"cancel"` + Reactivate Action `json:"reactivate"` + Report Action `json:"report"` + Reuse ReuseInfo `json:"reuse"` + Sale TextVerifiedResp `json:"sale"` + ServiceName string `json:"serviceName"` + // State 表示验证或服务的当前状态。 + // 状态是以下枚举值之一,代表了从创建到完成或取消的生命周期: + // * verificationPending: 正在等待验证码。 + // * verificationCompleted: 验证已成功完成。 + // * verificationCanceled: 验证被取消。 + // * verificationTimedOut: 验证因超时而失败。 + // * verificationReported: 验证被举报。 + // * verificationRefunded: 验证已退款。 + // * verificationReused: 验证号码被重用。 + // * verificationReactivated: 验证号码被重新激活。 + // + // 对于可续订服务: + // * renewableActive: 服务活跃中。 + // * renewableOverdue: 服务已逾期。 + // * renewableExpired: 服务已过期。 + // * renewableRefunded: 服务已退款。 + // + // 对于不可续订服务: + // * 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"` +} + +// 长效详情 +type VerificationRentalDetailResp struct { + Number string `json:"number"` + SMS TextVerifiedResp `json:"sms"` + Calls TextVerifiedResp `json:"calls"` + CreatedAt time.Time `json:"createdAt"` + ID string `json:"id"` + Sale TextVerifiedResp `json:"sale"` + SaleId string `json:"saleId"` + ServiceName string `json:"serviceName"` + State string `json:"state"` + AlwaysOn bool `json:"alwaysOn"` +} + +// 包含可执行标志 + 链接 +type Action struct { + CanCancel bool `json:"canCancel,omitempty"` + CanReactivate bool `json:"canReactivate,omitempty"` + CanReport bool `json:"canReport,omitempty"` + Link TextVerifiedResp `json:"link"` +} + +// reuse 特殊结构 +type ReuseInfo struct { + Link TextVerifiedResp `json:"link"` + ReusableUntil *time.Time `json:"reusableUntil"` // null -> 指针可为空 +} + +type TextVerifiedPriceReq struct { + ServiceName string `json:"serviceName" comment:"服务code"` + AreaCode bool `json:"areaCode" comment:"限制区域code"` + NumberType string `json:"numberType" comment:"mobile | voip | landline"` + Capability string `json:"capability" comment:"sms | mms | voice"` + AlwaysOn bool `json:"alwaysOn"` + IsRenewable bool `json:"isRenewable" comment:"是否可再生"` + Duration string `json:"duration" comment:"oneDay┃threeDay┃sevenDay┃fourteenDay┃thirtyDay┃ninetyDay┃oneYear"` +} + +type TextVerifiedPriceResp struct { + Price decimal.Decimal `json:"price"` + ServiceName string `json:"serviceName"` +} + +type TextVerifiedSmsResp struct { + Data []TextVerifiedSmsItemResp `json:"data"` +} + +type TextVerifiedSmsItemResp struct { + Id string `json:"id"` + From string `json:"from"` + To string `json:"to"` + CreatedAt string `json:"createdAt"` + SmsContent string `json:"smsContent"` + ParsedCode string `json:"parsedCode"` + Encrypted bool `json:"encrypted"` +} diff --git a/app/admin/service/member_api.go b/app/admin/service/member_api.go new file mode 100644 index 0000000..ffb92e1 --- /dev/null +++ b/app/admin/service/member_api.go @@ -0,0 +1,110 @@ +package service + +import ( + "fmt" + "go-admin/app/admin/models" + "go-admin/app/admin/service/dto" + "go-admin/common/rediskey" + "go-admin/utils/redishelper" + "go-admin/utils/utility" + + "github.com/bytedance/sonic" + "github.com/go-admin-team/go-admin-core/sdk/service" + "github.com/jinzhu/copier" +) + +type MemberApi struct { + service.Service +} + +func (e MemberApi) ChangeStatus(id, userId int, status int) error { + var api models.MemberApi + + if err := e.Orm.Model(&api).Where("id =? and user_id =?", id, userId).Update("status", status).Error; err != nil { + return err + } + + return nil +} + +// GetByUserId 通过用户ID获取用户信息 +func (e *MemberApi) GetByUserId(userId int, memberData *dto.MemberApiResp) error { + var entity models.MemberApi + + if err := e.Orm.Model(&models.MemberApi{}).Where("user_id = ?", userId).First(&entity).Error; err != nil { + e.Log.Errorf("通过用户ID获取用户信息失败, %s", err) + } + + copier.Copy(memberData, &entity) + + return nil +} + +// 初始化用户API +func (e *MemberApi) InitApis() error { + var entitys []models.SysUser + var memberApis []models.MemberApi + + if err := e.Orm.Model(&models.SysUser{}). + Joins("LEFT JOIN member_api ON member_api.user_id = sys_user.user_id"). + Where("member_api.id is null").Find(&entitys).Error; err != nil { + e.Log.Errorf("初始化 查询用户API失败, %s", err) + return err + } + + var data models.MemberApi + for _, entity := range entitys { + e.CreateApi(entity.UserId, &data, 0) + } + + if err := e.Orm.Model(&models.MemberApi{}).Find(&memberApis).Error; err != nil { + e.Log.Errorf("初始化 查询用户API失败, %s", err) + return err + } + + for _, item := range memberApis { + key := fmt.Sprintf(rediskey.MemberApiKey, item.ApiKey) + val, err := sonic.MarshalString(item) + + if err != nil { + e.Log.Errorf("用户api 初始化失败") + continue + } + + if err := redishelper.DefaultRedis.SetString(key, val); err != nil { + e.Log.Errorf("用户api 初始化失败") + continue + } + } + return nil +} + +// 创建api +func (e *MemberApi) CreateApi(userId int, user *models.MemberApi, retryCount int) error { + key, err := utility.GenerateBase62Key(32) + + if err != nil { + e.Log.Errorf("生成API Key失败, %s", err) + return err + } + + apiEntity := models.MemberApi{ + UserId: userId, + ApiKey: key, + Status: 1, // 默认启用 + } + + var count int64 + e.Orm.Model(user).Where("api_key =?", key).Count(&count) + + if count > 0 && retryCount < 1000 { + return e.CreateApi(userId, user, retryCount+1) + } + + if err := e.Orm.Create(&apiEntity).Error; err != nil { + e.Log.Errorf("初始化用户API失败, %s", err) + return err + } + + return nil +} diff --git a/app/admin/service/open_common.go b/app/admin/service/open_common.go new file mode 100644 index 0000000..bb27ba5 --- /dev/null +++ b/app/admin/service/open_common.go @@ -0,0 +1 @@ +package service diff --git a/app/admin/service/sms_daisysms.go b/app/admin/service/sms_daisysms.go new file mode 100644 index 0000000..c1a742f --- /dev/null +++ b/app/admin/service/sms_daisysms.go @@ -0,0 +1,355 @@ +package service + +import ( + "errors" + "fmt" + "go-admin/app/admin/models" + "go-admin/app/admin/service/dto" + "go-admin/common/global" + "go-admin/common/statuscode" + "go-admin/config" + "go-admin/utils/httphelper" + "net/http" + "strconv" + "strings" + "time" + + "github.com/bytedance/sonic" + "github.com/go-admin-team/go-admin-core/sdk/service" + "github.com/shopspring/decimal" +) + +type SmsDaisysms struct { + service.Service +} + +// 同步价格 +func (e SmsDaisysms) SyncPrices() { + prices, err := e.GetPrices() + + if err != nil { + e.Log.Errorf("GetPrices error: %v", err) + return + } + + for _, price := range prices { + if err := e.Orm.Model(&models.SmsServices{}).Where("code =? and platform_code=?", price.Key, global.SmsPlatformDaisysms).Update("price", price.Cost).Error; err != nil { + e.Log.Errorf("Update price error: %v", err) + } + } +} + +// GetNumberForApi 获取短期或长期租赁号码 +// getType 0-短效 1-长效 +// service 服务code +// maxPrice 最大价格 +// period 时长(月) +func (e *SmsDaisysms) GetNumberForApi(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) + + if getType == 1 { + url = fmt.Sprintf("%s&duration=%dM", url, period) + } + + client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) + //ACCESS_NUMBER:999999:13476711222 + bytes, _, err := client.GetRaw(url, nil) + + if err != nil { + e.Log.Errorf("租赁请求失败 %s", err.Error()) + + if strings.Contains(err.Error(), "NO_NUMBERS") { + return acitvationId, result, statuscode.SmsOutOfStockOrUnavailable + } else { + return acitvationId, result, statuscode.ServerError + } + } + content := string(bytes) + + if strings.Contains(content, "ACCESS_NUMBER:") { + if len(strings.Split(content, ":")) < 2 { + e.Log.Errorf("租赁请求失败 %s", content) + return acitvationId, result, statuscode.ServerError + } + + acitvationId, _ = strconv.Atoi(strings.Split(content, ":")[1]) + result = strings.Split(content, ":")[2] + } else if strings.Contains(content, "MAX_PRICE_EXCEEDED:") { + e.Log.Errorf("租赁价格超过最大价格") + resultCode = statuscode.NoNumbers + } else if strings.Contains(content, "NO_NUMBERS") { + e.Log.Error("平台号码不足") + resultCode = statuscode.NoNumbers + } else if strings.Contains(content, "TOO_MANY_ACTIVE_RENTALS") { + e.Log.Error("租赁数量超过限制") + resultCode = statuscode.ServerError + } else if strings.Contains(content, "NO_MONEY") { + e.Log.Error("余额不足") + resultCode = statuscode.BalanceNotEnough + } else { + e.Log.Errorf("租赁请求失败 %s", content) + resultCode = statuscode.ServerError + } + + return acitvationId, result, resultCode +} + +// 设置租赁结束 +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) + client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) + bytes, _, err := client.GetRaw(url, nil) + + if err != nil { + e.Log.Errorf("租赁请求失败 %s", err.Error()) + return statuscode.ServerError + } + content := string(bytes) + + if content == "ACCESS_ACTIVATION" { + return statuscode.Success + } else { + e.Log.Errorf("租赁请求失败 %s", content) + return statuscode.ServerError + } +} + +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) { + result := "" + key, code := GetApiKey(e) + + if code != statuscode.Success { + return result, code + } + url := fmt.Sprintf("?api_key=%s&action=getStatus&id=%s", key.ConfigValue, messageId) + client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) + bytes, _, err := client.GetRaw(url, nil) + + if err != nil { + e.Log.Errorf("租赁请求失败 %s", err.Error()) + return result, statuscode.ServerError + } + content := string(bytes) + + if strings.Contains(content, "STATUS_OK:") && content != "STATUS_OK:KEEP" { + result = strings.Split(content, ":")[1] + } else if content == "NO_ACTIVATION" { + code = statuscode.SmsNoActivation + } else if content == "STATUS_WAIT_CODE" || content == "STATUS_OK:KEEP" { + code = statuscode.SmsWaitCode + } else if content == "STATUS_CANCEL" { + code = statuscode.SmsCancel + } else { + e.Log.Errorf("租赁请求失败 %s", content) + return result, statuscode.ServerError + } + + return result, code +} + +// getExtraActivation 获取额外的激活 +// messageId 短信id +// return 验证码, 状态码 +func (e *SmsDaisysms) getExtraActivation(activationId string) (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) + client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) + bytes, _, err1 := client.GetRaw(url, nil) + + if err1 != nil { + e.Log.Errorf("租赁请求失败 %s", err1.Error()) + return 0, statuscode.ServerError + } + + content := string(bytes) + + if strings.Contains(content, "ASLEEP:") { + message := strings.Split(content, ":")[1] + result, _ = strconv.Atoi(message) + + if result > 0 { + return result, statuscode.Success + } + } else if strings.Contains(content, "STATUS_OK:") { + return 0, statuscode.SmsLongNumWaitCode + } else if strings.Contains(content, "ACCESS_NUMBER:") { + message := strings.Split(content, ":")[1] + result, _ = strconv.Atoi(message) + + if result > 0 { + return result, statuscode.Success + } + } + + e.Log.Errorf("激活长效号码失败,%s", content) + + return 0, statuscode.ServerError +} + +// KeepLongTerm 长期租赁 +func (e *SmsDaisysms) KeepLongTerm(activationId string) int { + key, code := GetApiKey(e) + + if code != statuscode.Success { + return statuscode.ServerError + } + + url := fmt.Sprintf("?api_key=%s&action=keep&id=%s", key.ConfigValue, activationId) + client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) + bytes, _, err := client.GetRaw(url, nil) + + if err != nil { + e.Log.Errorf("租赁请求失败 %s", err.Error()) + return statuscode.ServerError + } + content := string(bytes) + + if content == "OK" { + return statuscode.Success + } else { + e.Log.Errorf("租赁请求失败 %s", content) + return statuscode.ServerError + } +} + +// 取消租赁 +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) + client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) + bytes, _, err := client.GetRaw(url, nil) + + if err != nil { + e.Log.Errorf("租赁请求失败 %s", err.Error()) + return statuscode.ServerError + } + content := string(bytes) + + if content == "ACCESS_CANCEL" { + return statuscode.Success + } else { + e.Log.Errorf("租赁请求失败 %s", content) + return statuscode.ServerError + } +} + +// 获取价格 +func (e *SmsDaisysms) GetPrices() ([]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) + client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) + bytes, status, err := client.GetRaw(url, nil) + + if err != nil { + e.Log.Errorf("租赁请求失败 %s", err.Error()) + return result, err + } + + mapData := make(map[string]map[string]dto.DaisysmsPriceResp) + err = sonic.Unmarshal(bytes, &mapData) + + if err != nil { + e.Log.Errorf("解析失败 %s", err.Error()) + return result, err + } + + rawData, ok := mapData["187"] + + if !ok { + e.Log.Errorf("获取 187数据失败") + + return result, errors.New("获取 187数据失败") + } + + if status != http.StatusOK { + return result, fmt.Errorf("获取价格失败 %d", status) + } + + for key, v := range rawData { + v.Key = key + + result = append(result, v) + } + + return result, nil +} + +// ChangeAutoRenew 修改自动续期 +// activationId 短信id +// status 状态 +func (e *SmsDaisysms) ChangeAutoRenewForApi(activationId string, status bool) int { + key, err := GetApiKey(e) + + 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) + client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) + bytes, _, err1 := client.GetRaw(url, nil) + + if err1 != nil { + e.Log.Errorf("租赁请求失败 %s", err1.Error()) + return statuscode.ServerError + } + + content := string(bytes) + + if content == "OK" { + return statuscode.Success + } else { + e.Log.Errorf("修改自动续期请求失败 id:%d, status:%t, %s", activationId, status, content) + return statuscode.ServerError + } +} diff --git a/app/admin/service/sms_phone.go b/app/admin/service/sms_phone.go index b80dfe0..76c4420 100644 --- a/app/admin/service/sms_phone.go +++ b/app/admin/service/sms_phone.go @@ -2,9 +2,8 @@ package service import ( "errors" - "fmt" + "net/http" "strconv" - "strings" "time" "github.com/go-admin-team/go-admin-core/sdk/service" @@ -17,28 +16,55 @@ import ( cDto "go-admin/common/dto" "go-admin/common/global" "go-admin/common/statuscode" - "go-admin/config" - "go-admin/utils/httphelper" ) type SmsPhone struct { service.Service } +// open-API 获取电话号码 +func (e SmsPhone) OpenGetNumber(req *dto.GetNumberReq, userId int) (dto.OpenGetNumberResp, int) { + resp := dto.OpenGetNumberResp{} + balanceService := MemberBalance{Service: e.Service} + balance, smsPhone, i := e.DoGetNumber(&balanceService, req, userId) + + resp.Blance = balance + if i != statuscode.Success { + return resp, i + } + + resp.ActivationId = smsPhone.NewActivationId + resp.ExpireTime = smsPhone.ExpireTime + resp.Number = smsPhone.Phone + + return resp, statuscode.Success +} + // 取消等待 func (e *SmsPhone) CancelNumber(req *dto.SmsPhoneCancelNumberReq, userId int) int { var data models.SmsPhone - if err := e.Orm.Model(data).Where("id =? and user_id= ?", req.Id, userId).First(&data).Error; err != nil { - e.Log.Errorf("获取短信号码失败, %s", err) - return statuscode.SmsNotExisted + + if req.Id > 0 { + if err := e.Orm.Model(data).Where("id =? and user_id= ?", req.Id, userId).First(&data).Error; err != nil { + e.Log.Errorf("获取短信号码失败, %s", err) + return statuscode.SmsNotExisted + } + } else if req.ActivationId != "" { + if err := e.Orm.Model(data).Where("new_activation_id =? and user_id= ?", req.ActivationId, userId).First(&data).Error; err != nil { + e.Log.Errorf("获取短信号码失败, %s", err) + return statuscode.SmsNotExisted + } + } else { + e.Log.Errorf("id和activationId 都是空") + return statuscode.ServerError } if data.Actived == 1 { - code := e.CancelRental(data.NewActivationId) + code := e.CancelNumberManage(&data) if code == statuscode.Success { err := e.Orm.Transaction(func(tx *gorm.DB) error { - if err1 := e.Orm.Model(data).Updates(map[string]interface{}{"status": 3, "code": "", "actived": "3"}).Error; err1 != nil { + if err1 := e.Orm.Model(data).Updates(map[string]interface{}{"status": 3, "code": "", "actived": "3", "auto_renewal": 2}).Error; err1 != nil { e.Log.Errorf("更新短信号码失败, %s", err1) return err1 } @@ -63,6 +89,23 @@ func (e *SmsPhone) CancelNumber(req *dto.SmsPhoneCancelNumberReq, userId int) in return statuscode.Success } +// 聚合取消 +func (e *SmsPhone) CancelNumberManage(data *models.SmsPhone) int { + switch data.PlatformCode { + case global.SmsPlatformDaisysms: + service := SmsDaisysms{Service: e.Service} + + return service.CancelRental(data.NewActivationId) + case global.SmsPlatformTextVerified: + service := SmsTextVerified{Service: e.Service} + + return service.CancelRental(data.NewActivationId, data.Type) + default: + return statuscode.SmsPlatformUnavailable + } + +} + // 删除断信号码 func (e *SmsPhone) DeleteMyNumber(req *dto.DeleteMyNumberReq, userId int) int { var data models.SmsPhone @@ -91,35 +134,56 @@ func (e *SmsPhone) DeleteMyNumber(req *dto.DeleteMyNumberReq, userId int) int { } // 唤醒长效号码 -func (e *SmsPhone) WeakUp(req *dto.WeakUpReq, 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 { e.Log.Errorf("获取短信号码失败, %s", err) - return statuscode.ServerError + return result, statuscode.ServerError } - if smsPhone.Status != 1 { - newActivationId, code := e.getExtraActivation(smsPhone.ActivationId) + if smsPhone.Status != 1 || defult { + newActivationId, messageId, code := e.WeakUpManage(&smsPhone) if code == statuscode.ServerError { - return code + return result, code } else if code == statuscode.Success { - if err := e.Orm.Model(smsPhone).Updates(map[string]interface{}{"new_activation_id": newActivationId, "status": 1, "code": ""}).Error; err != nil { + if err := e.Orm.Model(smsPhone).Updates(map[string]interface{}{"new_activation_id": newActivationId, "status": 1, "code": "", "message_id": messageId}).Error; err != nil { e.Log.Errorf("更新短信号码失败, %s", err) - return statuscode.ServerError + return result, statuscode.ServerError } + + result.ActivationId = newActivationId + result.MessageId = messageId } else if code == statuscode.SmsLongNumWaitCode { e.Log.Info("无须唤醒") if err := e.Orm.Model(smsPhone).Updates(map[string]interface{}{"status": 1, "code": ""}).Error; err != nil { e.Log.Errorf("更新短信号码失败, %s", err) - return statuscode.ServerError + return result, statuscode.ServerError } - return statuscode.Success + return result, statuscode.Success } } - return statuscode.Success + return result, statuscode.Success +} + +// 唤醒号码 +// returns newActivationId,messageId, code +func (e *SmsPhone) WeakUpManage(req *models.SmsPhone) (string, string, int) { + switch req.PlatformCode { + case global.SmsPlatformDaisysms: + service := SmsDaisysms{Service: e.Service} + id, code := service.getExtraActivation(req.ActivationId) + return strconv.Itoa(id), "", code + case global.SmsPlatformTextVerified: + service := SmsTextVerified{Service: e.Service} + + return service.getExtraActivation(req.NewActivationId) + default: + return "", "", statuscode.SmsPlatformUnavailable + } } // 分页查询自己的号码列表 @@ -142,64 +206,89 @@ func (e *SmsPhone) GetMyPage(req *dto.SmsPhoneGetPageReq, userId int, phone *[]m // 租赁号码 func (e *SmsPhone) GetNumber(req *dto.GetNumberReq, userId int) (decimal.Decimal, int) { + balanceService := MemberBalance{Service: e.Service} + balance, _, i := e.DoGetNumber(&balanceService, req, userId) + if i != statuscode.Success { + return balance, i + } + + balance = balanceService.GetBalance(userId) + + return balance, statuscode.Success +} + +func (e *SmsPhone) DoGetNumber(balanceService *MemberBalance, req *dto.GetNumberReq, userId int) (decimal.Decimal, models.SmsPhone, int) { config := dto.GetSysConfigByKEYForServiceResp{} configReq := dto.SysConfigByKeyReq{} configService := SysConfig{Service: e.Service} - - if strings.ToLower(req.ServiceCode) != "wa" { - return decimal.Zero, statuscode.SmsServiceUnavailable - } - - if req.Type == 0 { - configReq.ConfigKey = "number_fee_short_term" - } else { - configReq.ConfigKey = "number_fee_long_term" - } - err := configService.GetWithKey(&configReq, &config) - - if err != nil { - return decimal.Zero, statuscode.ServerError - } - - if config.ConfigValue == "" { - e.Log.Errorf("短期或长期租赁费用不能为空") - return decimal.Zero, statuscode.ServerError - } - - price, err := decimal.NewFromString(config.ConfigValue) - if err != nil { - e.Log.Errorf("短期或长期租赁费用格式错误") - return decimal.Zero, statuscode.ServerError - } - - smsService := SmsServices{Service: e.Service} - balanceService := MemberBalance{Service: e.Service} - balance := balanceService.GetBalance(userId) - serviceItem, err := smsService.GetByCode(req.ServiceCode) + smsServices := SmsServices{Service: e.Service} + var price decimal.Decimal + serviceItem, err := smsServices.GetByCode(req.PlatformCode, req.ServiceCode) if err != nil { e.Log.Errorf("短信服务报错:%v", err) - return decimal.Zero, statuscode.ServerError + return decimal.Decimal{}, models.SmsPhone{}, statuscode.ServerError } if serviceItem.Code == "" { e.Log.Error("短信服务不存在") - return decimal.Zero, statuscode.ServerError + return decimal.Decimal{}, models.SmsPhone{}, statuscode.ServerError } + switch { + case req.Type == 0 && req.PlatformCode == global.SmsPlatformDaisysms: + price = serviceItem.Price + configReq.ConfigKey = "number_premium_daisysms" + case req.Type == 0 && req.PlatformCode == global.SmsPlatformTextVerified: + price = serviceItem.Price + configReq.ConfigKey = "number_premium_textverified" + case req.Type == 1 && req.PlatformCode == global.SmsPlatformDaisysms: + price = serviceItem.LongPrice + configReq.ConfigKey = "long_number_premium_daisysms" + case req.Type == 1 && req.PlatformCode == global.SmsPlatformTextVerified: + price = serviceItem.LongPrice + configReq.ConfigKey = "long_number_premium_textverified" + } + err = configService.GetWithKey(&configReq, &config) + + if err != nil { + return decimal.Decimal{}, models.SmsPhone{}, statuscode.ServerError + } + + if config.ConfigValue == "" { + e.Log.Errorf("短期长期租赁浮动百分比不能为空") + return decimal.Decimal{}, models.SmsPhone{}, statuscode.ServerError + } + + percent, err := decimal.NewFromString(config.ConfigValue) + if err != nil { + e.Log.Errorf("短期或长期租赁费用格式错误") + return decimal.Decimal{}, models.SmsPhone{}, statuscode.ServerError + } + + price = price.Mul(decimal.NewFromInt(100).Add(percent)).Div(decimal.NewFromInt(100)) + + if price.Cmp(decimal.Zero) <= 0 { + e.Log.Errorf("短期或长期租赁费用不能小于等于0") + return decimal.Decimal{}, models.SmsPhone{}, statuscode.ServerError + } + + balance := balanceService.GetBalance(userId) + if balance.LessThan(price) { e.Log.Error("余额不足") - return decimal.Zero, statuscode.BalanceNotEnough + return decimal.Decimal{}, models.SmsPhone{}, statuscode.BalanceNotEnough } now := time.Now() - activationId, phone, code := e.GetNumberForApi(req.Type, req.ServiceCode, price, req.Period) + activationId, phone, code, expireTime := e.GetNumberManage(req.PlatformCode, req.Type, req.ServiceCode, price, req.Period) if code != statuscode.Success { - return decimal.Zero, code + return decimal.Decimal{}, models.SmsPhone{}, code } smsPhone := models.SmsPhone{} + smsPhone.PlatformCode = req.PlatformCode smsPhone.Phone = phone smsPhone.UserId = userId smsPhone.Service = serviceItem.Name @@ -213,30 +302,45 @@ func (e *SmsPhone) GetNumber(req *dto.GetNumberReq, userId int) (decimal.Decimal smsPhone.Price = price if req.Type == 1 { - days := req.Period * 30 - now = now.AddDate(0, 0, days) - smsPhone.ExpireTime = &now + if expireTime != nil { + smsPhone.ExpireTime = expireTime + } else { + days := req.Period * 30 + now = now.AddDate(0, 0, days) + smsPhone.ExpireTime = &now + } + } else { - now = now.Add(time.Minute * time.Duration(serviceItem.ExpirationMinutes)) - smsPhone.ExpireTime = &now - smsPhone.AutoRenewal = 2 + if expireTime != nil { + smsPhone.ExpireTime = expireTime + } else { + now = now.Add(time.Minute * time.Duration(serviceItem.ExpirationMinutes)) + smsPhone.ExpireTime = &now + smsPhone.AutoRenewal = 2 + } } smsPhone.Status = 1 if err := e.Orm.Save(&smsPhone).Error; err != nil { e.Log.Errorf("获取手机号失败", err) - return decimal.Zero, statuscode.ServerError + return decimal.Decimal{}, models.SmsPhone{}, statuscode.ServerError } if err := e.Orm.Exec("UPDATE member_balance SET balance = balance -? WHERE user_id =?", price, userId).Error; err != nil { e.Log.Errorf("更新余额失败", err) - return decimal.Zero, statuscode.ServerError + return decimal.Decimal{}, models.SmsPhone{}, statuscode.ServerError } //长效号码默认开启续费 if req.Type == 1 { - code := e.ChangeAutoRenewForApi(smsPhone.NewActivationId, true) + var code int + if req.PlatformCode == global.SmsPlatformDaisysms { + code = e.ChangeAutoRenewManage(smsPhone.PlatformCode, smsPhone.NewActivationId, true) + } else { + code = http.StatusOK + e.WeakUp(&dto.WeakUpReq{ActivationId: smsPhone.NewActivationId}, userId, true) + } if code == statuscode.Success { smsPhone.AutoRenewal = 1 @@ -247,116 +351,36 @@ func (e *SmsPhone) GetNumber(req *dto.GetNumberReq, userId int) (decimal.Decimal } } - balance = balanceService.GetBalance(userId) - - return balance, statuscode.Success + return balance, smsPhone, statuscode.Success } -// GetNumberForApi 获取短期或长期租赁号码 -// getType 0-短效 1-长效 -// service 服务code -// maxPrice 最大价格 -// period 时长(月) -func (e *SmsPhone) GetNumberForApi(getType int, serviceCode string, maxPrice decimal.Decimal, period int) (int, string, int) { - acitvationId := 0 - result := "" - resultCode := statuscode.Success +// 租赁号码 +func (e *SmsPhone) GetNumberManage(platformCode string, typ int, serviceCode string, price decimal.Decimal, period int) (string, string, int, *time.Time) { + switch platformCode { + case global.SmsPlatformDaisysms: + service := SmsDaisysms{Service: e.Service} + activationId, phone, code := service.GetNumberForApi(typ, serviceCode, price, period) - 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) + return strconv.Itoa(activationId), phone, code, nil + case global.SmsPlatformTextVerified: + service := SmsTextVerified{Service: e.Service} + resp, code := service.GetNumberForApi(typ, serviceCode, price, period) - if getType == 1 { - url = fmt.Sprintf("%s&duration=%dM", url, period) - } - - client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) - //ACCESS_NUMBER:999999:13476711222 - bytes, err := client.GetRaw(url, nil) - - if err != nil { - e.Log.Errorf("租赁请求失败 %s", err.Error()) - return acitvationId, result, statuscode.ServerError - } - content := string(bytes) - - if strings.Contains(content, "ACCESS_NUMBER:") { - if len(strings.Split(content, ":")) < 2 { - e.Log.Errorf("租赁请求失败 %s", content) - return acitvationId, result, statuscode.ServerError + if code != statuscode.Success { + return "", "", code, nil } - acitvationId, _ = strconv.Atoi(strings.Split(content, ":")[1]) - result = strings.Split(content, ":")[2] - } else if strings.Contains(content, "MAX_PRICE_EXCEEDED:") { - e.Log.Errorf("租赁价格超过最大价格") - resultCode = statuscode.NoNumbers - } else if strings.Contains(content, "NO_NUMBERS") { - e.Log.Error("平台号码不足") - resultCode = statuscode.NoNumbers - } else if strings.Contains(content, "TOO_MANY_ACTIVE_RENTALS") { - e.Log.Error("租赁数量超过限制") - resultCode = statuscode.ServerError - } else if strings.Contains(content, "NO_MONEY") { - e.Log.Error("余额不足") - resultCode = statuscode.BalanceNotEnough - } else { - e.Log.Errorf("租赁请求失败 %s", content) - resultCode = statuscode.ServerError + return resp.Id, resp.Phone, code, resp.EndAt + default: + return "", "", statuscode.SmsPlatformUnavailable, nil } - - return acitvationId, result, resultCode -} - -// 设置租赁结束 -func (e *SmsPhone) 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) - client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) - bytes, err := client.GetRaw(url, nil) - - if err != nil { - e.Log.Errorf("租赁请求失败 %s", err.Error()) - return statuscode.ServerError - } - content := string(bytes) - - if content == "ACCESS_ACTIVATION" { - return statuscode.Success - } else { - e.Log.Errorf("租赁请求失败 %s", content) - return statuscode.ServerError - } -} - -func GetApiKey(e *SmsPhone) (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 } func (e *SmsPhone) GetCodeByActivationId(req *dto.GetCodeReq, userId int) ([]dto.GetCodeResp, int) { var smsPhone []models.SmsPhone result := []dto.GetCodeResp{} - if err := e.Orm.Model(models.SmsPhone{}).Where("user_id =? and activation_id in ?", userId, req.ActivationIds).Find(&smsPhone).Error; err != nil { + if err := e.Orm.Model(models.SmsPhone{}).Where("user_id =? and new_activation_id in ?", userId, req.ActivationIds).Find(&smsPhone).Error; err != nil { return result, statuscode.ServerError } @@ -392,7 +416,18 @@ func (e *SmsPhone) SyncCodes() error { } for _, item := range phones { - code, codeStatus := e.GetCodeForApi(item.NewActivationId) + code, codeStatus := e.GetCodeManage(item.PlatformCode, item.NewActivationId, item.Type, item.UpdatedAt.Unix()) + + if code == "" { + expireTime := item.UpdatedAt.Add(time.Second * 150) + if item.PlatformCode == global.SmsPlatformTextVerified && expireTime.Before(time.Now()) { + if err := e.Orm.Model(&item).Updates(map[string]interface{}{"status": 3, "updated_at": time.Now()}).Error; err != nil { + e.Log.Errorf("手机号一睡眠 %s", item.Phone) + } + } + + continue + } mapData := make(map[string]interface{}) logMapData := make(map[string]interface{}) @@ -437,135 +472,18 @@ func (e *SmsPhone) SyncCodes() error { return nil } -// GetCodeForApi 获取验证码 -// messageId 短信id -// return 验证码, 状态码 -func (e *SmsPhone) GetCodeForApi(messageId int) (string, int) { - result := "" - key, code := GetApiKey(e) +// 获取验证码 +func (e *SmsPhone) GetCodeManage(platformCode string, messageId string, typ int, unixTime int64) (string, int) { + switch platformCode { + case global.SmsPlatformDaisysms: + service := SmsDaisysms{Service: e.Service} - if code != statuscode.Success { - return result, code - } - url := fmt.Sprintf("?api_key=%s&action=getStatus&id=%d", key.ConfigValue, messageId) - client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) - bytes, err := client.GetRaw(url, nil) - - if err != nil { - e.Log.Errorf("租赁请求失败 %s", err.Error()) - return result, statuscode.ServerError - } - content := string(bytes) - - if strings.Contains(content, "STATUS_OK:") && content != "STATUS_OK:KEEP" { - result = strings.Split(content, ":")[1] - } else if content == "NO_ACTIVATION" { - code = statuscode.SmsNoActivation - } else if content == "STATUS_WAIT_CODE" || content == "STATUS_OK:KEEP" { - code = statuscode.SmsWaitCode - } else if content == "STATUS_CANCEL" { - code = statuscode.SmsCancel - } else { - e.Log.Errorf("租赁请求失败 %s", content) - return result, statuscode.ServerError - } - - return result, code -} - -// getExtraActivation 获取额外的激活 -// messageId 短信id -// return 验证码, 状态码 -func (e *SmsPhone) getExtraActivation(activationId int) (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=%d", key.ConfigValue, activationId) - client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) - bytes, err1 := client.GetRaw(url, nil) - - if err1 != nil { - e.Log.Errorf("租赁请求失败 %s", err1.Error()) - return 0, statuscode.ServerError - } - - content := string(bytes) - - if strings.Contains(content, "ASLEEP:") { - message := strings.Split(content, ":")[1] - result, _ = strconv.Atoi(message) - - if result > 0 { - return result, statuscode.Success - } - } else if strings.Contains(content, "STATUS_OK:") { - return 0, statuscode.SmsLongNumWaitCode - } else if strings.Contains(content, "ACCESS_NUMBER:") { - message := strings.Split(content, ":")[1] - result, _ = strconv.Atoi(message) - - if result > 0 { - return result, statuscode.Success - } - } - - e.Log.Errorf("激活长效号码失败,%s", content) - - return 0, statuscode.ServerError -} - -// KeepLongTerm 长期租赁 -func (e *SmsPhone) KeepLongTerm(activationId int) int { - key, code := GetApiKey(e) - - if code != statuscode.Success { - return statuscode.ServerError - } - - url := fmt.Sprintf("?api_key=%s&action=keep&id=%d", key.ConfigValue, activationId) - client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) - bytes, err := client.GetRaw(url, nil) - - if err != nil { - e.Log.Errorf("租赁请求失败 %s", err.Error()) - return statuscode.ServerError - } - content := string(bytes) - - if content == "OK" { - return statuscode.Success - } else { - e.Log.Errorf("租赁请求失败 %s", content) - return statuscode.ServerError - } -} - -// 取消租赁 -func (e *SmsPhone) CancelRental(activationId int) 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=%d&status=8", key.ConfigValue, activationId) - client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) - bytes, err := client.GetRaw(url, nil) - - if err != nil { - e.Log.Errorf("租赁请求失败 %s", err.Error()) - return statuscode.ServerError - } - content := string(bytes) - - if content == "ACCESS_CANCEL" { - return statuscode.Success - } else { - e.Log.Errorf("租赁请求失败 %s", content) - return statuscode.ServerError + return service.GetCodeForApi(messageId) + case global.SmsPlatformTextVerified: + service := SmsTextVerified{Service: e.Service} + return service.GetCode(messageId, typ, unixTime) + default: + return "", statuscode.SmsPlatformUnavailable } } @@ -637,64 +555,74 @@ func (e SmsServices) AutoRenewal() error { func (e *SmsPhone) ChangeAutoRenew(req *dto.SmsPhoneChangeAutoRenewReq, userId int) int { var data models.SmsPhone - if err := e.Orm.Model(data).Where("id =? and user_id =?", req.Id, userId).First(&data).Error; err != nil { - e.Log.Errorf("修改自动续期 数据不存在 id:%d err:%s", req.Id, err.Error()) - return statuscode.SmsNotExisted + if req.Id > 0 { + if err := e.Orm.Model(data).Where("id =? and user_id =?", req.Id, userId).First(&data).Error; err != nil { + e.Log.Errorf("修改自动续期 数据不存在 id:%d err:%s", req.Id, err.Error()) + return statuscode.SmsNotExisted + } + } else if req.ActivationId != "" { + if err := e.Orm.Model(data).Where("activation_id =? and user_id =?", req.ActivationId, userId).First(&data).Error; err != nil { + e.Log.Errorf("修改自动续期 数据不存在 id:%d err:%s", req.Id, err.Error()) + return statuscode.SmsNotExisted + } + } else { + return statuscode.InvalidParams } // 短期租赁不支持自动续期 - if data.Type == 0 { - return statuscode.SmsNotAutoRenew - } - - status := false - changeAutoRenewal := 2 - if req.AutoRenew == 1 { - status = true - changeAutoRenewal = 1 - } - - data.AutoRenewal = req.AutoRenew - code := e.ChangeAutoRenewForApi(data.ActivationId, status) - - if code != statuscode.Success { - return statuscode.ServerError - } - - if err := e.Orm.Model(&data).Update("auto_renewal", changeAutoRenewal).Error; err != nil { - e.Log.Errorf("修改自动续期失败 id:%d, status:%t", data.ActivationId, status) + shouldReturn, i := e.DoChangeAutoRenewal(data, req.AutoRenew) + if shouldReturn { + return i } return statuscode.Success } -// ChangeAutoRenew 修改自动续期 -// activationId 短信id -// status 状态 -func (e *SmsPhone) ChangeAutoRenewForApi(activationId int, status bool) int { - key, err := GetApiKey(e) - - if err != statuscode.Success { - e.Log.Errorf("查询sms api请求失败 %d", activationId) - return statuscode.ServerError +// DoChangeAutoRenewal 执行修改自动续期 +func (e *SmsPhone) DoChangeAutoRenewal(data models.SmsPhone, autoRenewal int) (bool, int) { + if data.Type == 0 { + return true, statuscode.SmsNotAutoRenew } - url := fmt.Sprintf("?api_key=%s&action=setAutoRenew&id=%d&value=%t", key.ConfigValue, activationId, status) - client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil) - bytes, err1 := client.GetRaw(url, nil) - - if err1 != nil { - e.Log.Errorf("租赁请求失败 %s", err1.Error()) - return statuscode.ServerError + status := false + changeAutoRenewal := 2 + if autoRenewal == 1 { + status = true + changeAutoRenewal = 1 } - content := string(bytes) + var code int + data.AutoRenewal = autoRenewal - if content == "OK" { - return statuscode.Success + if data.Actived == 3 { + code = http.StatusOK } else { - e.Log.Errorf("修改自动续期请求失败 id:%d, status:%t, %s", activationId, status, content) - return statuscode.ServerError + code = e.ChangeAutoRenewManage(data.PlatformCode, data.ActivationId, status) + + if code != statuscode.Success { + return true, statuscode.ServerError + } + } + + if err := e.Orm.Model(&data).Update("auto_renewal", changeAutoRenewal).Error; err != nil { + e.Log.Errorf("修改自动续期失败 id:%d, status:%t", data.ActivationId, status) + } + return false, 0 +} + +// ChangeAutoRenewManage 修改自动续期管理 +func (e *SmsPhone) ChangeAutoRenewManage(platform string, activationId string, status bool) int { + switch platform { + case global.SmsPlatformDaisysms: + service := SmsDaisysms{Service: e.Service} + + return service.ChangeAutoRenewForApi(activationId, status) + case global.SmsPlatformTextVerified: + service := SmsTextVerified{Service: e.Service} + + return service.Renew(activationId, status) + default: + return statuscode.SmsPlatformUnavailable } } diff --git a/app/admin/service/sms_phone_test.go b/app/admin/service/sms_phone_test.go new file mode 100644 index 0000000..0404979 --- /dev/null +++ b/app/admin/service/sms_phone_test.go @@ -0,0 +1,65 @@ +package service + +import ( + "go-admin/app/admin/service/dto" + "go-admin/common/global" + "go-admin/common/statuscode" + "testing" + + "github.com/go-admin-team/go-admin-core/logger" +) + +func TestVerifiedGetNumber(t *testing.T) { + db := initSetting() + + service := SmsPhone{} + service.Orm = db + service.Log = logger.NewHelper(logger.DefaultLogger) + + if _, err := service.GetNumber(&dto.GetNumberReq{ + PlatformCode: global.SmsPlatformTextVerified, + Type: 1, + ServiceCode: "cocacola", + Period: 1, + }, 1); err != statuscode.Success { + t.Error(err) + } +} + +func TestVerifiedCancel(t *testing.T) { + db := initSetting() + + service := SmsPhone{} + service.Orm = db + service.Log = logger.NewHelper(logger.DefaultLogger) + + if err := service.CancelNumber(&dto.SmsPhoneCancelNumberReq{ + Id: 129, + }, 1); err != statuscode.Success { + t.Error(err) + } +} + +func TestVerifiedWake(t *testing.T) { + db := initSetting() + + service := SmsPhone{} + service.Orm = db + service.Log = logger.NewHelper(logger.DefaultLogger) + + service.WeakUp(&dto.WeakUpReq{ + ActivationId: "lr_01K21NVPVDEXTS1C3S8HPEC612", + }, 1, false) +} + +func TestGetPrices(t *testing.T) { + db := initSetting() + + service := SmsServices{} + service.Orm = db + service.Log = logger.NewHelper(logger.DefaultLogger) + + if err := service.SyncPrices(); err != nil { + t.Error(err) + } +} diff --git a/app/admin/service/sms_receive_log.go b/app/admin/service/sms_receive_log.go index 0dce2d6..f9b7d6c 100644 --- a/app/admin/service/sms_receive_log.go +++ b/app/admin/service/sms_receive_log.go @@ -48,7 +48,7 @@ func (e SmsReceiveLog) WebHook(req *dto.SmsReceiveWebHookReq) error { return err } - phoneService := SmsPhone{Service: e.Service} + phoneService := SmsDaisysms{Service: e.Service} if code := phoneService.setStatus(req.ActivationID); 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 5b4dc59..30b244f 100644 --- a/app/admin/service/sms_receive_log_test.go +++ b/app/admin/service/sms_receive_log_test.go @@ -12,14 +12,19 @@ import ( "gorm.io/gorm" ) -func initSetting() { +func initSetting() *gorm.DB { 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.TrxGridUrl = "https://api.trongrid.io" config.ExtConfig.DaisysmsUrl = "https://daisysms.com/stubs/handler_api.php" + config.ExtConfig.SmsTextVerified.ApiKey = "ZQ0swXnsaPpeGdwa3c7gT9U9I1Oh9WoDHx0amuYovvaHuqd5u6B4NBBUSUBjR" + config.ExtConfig.SmsTextVerified.UserName = "webspan@proton.me" + config.ExtConfig.SmsTextVerified.Url = "https://www.textverified.com" redishelper.InitDefaultRedis("127.0.0.1:6379", "", 4) redishelper.InitLockRedisConn("127.0.0.1:6379", "", "4") + + return db } func TestSmsReceiveLog(t *testing.T) { diff --git a/app/admin/service/sms_services.go b/app/admin/service/sms_services.go index 2576afd..06fa526 100644 --- a/app/admin/service/sms_services.go +++ b/app/admin/service/sms_services.go @@ -11,21 +11,75 @@ import ( "go-admin/app/admin/service/dto" "go-admin/common/actions" cDto "go-admin/common/dto" + "go-admin/common/global" ) type SmsServices struct { service.Service } +func (e SmsServices) OpenGetByCode(code string, param2 string, apiKey string) (any, error) { + panic("unimplemented") +} + +// 获取平台列表 +func (e SmsServices) GetPlatform(resp *[]dto.SmsPlatformGetListResp) error { + var datas []models.SysDictData + + if err := e.Orm.Model(&models.SysDictData{}).Where("dict_type = 'sms_platform'").Find(&datas).Error; err != nil { + return err + } + + for _, v := range datas { + var respDto dto.SmsPlatformGetListResp + respDto.Label = v.DictLabel + respDto.Value = v.DictValue + + *resp = append(*resp, respDto) + } + + return nil +} + // GetPrice 获取SmsServices价格 func (e SmsServices) GetPrice(req *dto.SmsGetPriceReq) (decimal.Decimal, error) { - key := "number_fee_short_term" + var service models.SmsServices - if req.Type == 1 { - key = "number_fee_long_term" + if err := e.Orm.Model(&service).Where("platform_code =? and code =?", req.PlatformCode, req.ServiceCode).First(&service).Error; err != nil { + return decimal.Decimal{}, errors.New("获取价格失败") } var price decimal.Decimal + var key string + + if req.Type == 1 { + price = service.Price + + switch req.PlatformCode { + case global.SmsPlatformDaisysms: + key = "long_number_premium_daisysms" + case global.SmsPlatformTextVerified: + key = "long_number_premium_textverified" + default: + return price, errors.New("获取价格失败") + } + } else { + price = service.LongPrice + + switch req.PlatformCode { + case global.SmsPlatformDaisysms: + key = "number_premium_daisysms" + case global.SmsPlatformTextVerified: + key = "number_premium_textverified" + default: + return price, errors.New("获取价格失败") + } + } + + if price.IsZero() { + return price, errors.New("获取价格失败") + } + configService := SysConfig{Service: e.Service} configResp := dto.GetSysConfigByKEYForServiceResp{} err := configService.GetWithKey(&dto.SysConfigByKeyReq{ConfigKey: key}, &configResp) @@ -34,29 +88,96 @@ func (e SmsServices) GetPrice(req *dto.SmsGetPriceReq) (decimal.Decimal, error) return price, err } - price, err = decimal.NewFromString(configResp.ConfigValue) + percent, err := decimal.NewFromString(configResp.ConfigValue) if err != nil { return price, err } + if percent.IsZero() { + return price, nil + } else { + price = price.Mul((decimal.NewFromInt(100).Add(percent)).Div(decimal.NewFromInt(100))).Truncate(2) + } + return price, nil } // GetList 获取SmsServices列表 func (e SmsServices) GetList(resp *[]dto.SmsServicesGetListResp) error { var data []models.SmsServices - err := e.Orm.Find(&data).Error + err := e.Orm.Model(&models.SmsServices{}).Where("status =1").Find(&data).Error if err != nil { e.Log.Errorf("SmsServicesService GetList error:%s \r\n", err) return err } + + dictService := SysDictData{Service: e.Service} + dictDatas, _ := dictService.GetMapByType("sms_platform") + + keys := []string{"number_premium_daisysms", "number_premium_textverified", "long_number_premium_daisysms", "long_number_premium_textverified"} + configService := SysConfig{Service: e.Service} + mapConfigs, err := configService.GetWithKeyForMap(keys) + if err != nil { + e.Log.Errorf("获取配置失败,\r\n失败信息 %s", err.Error()) + return err + } + for _, item := range data { respItem := dto.SmsServicesGetListResp{ - Name: item.Name, - Code: item.Code, - Status: item.Status, + Name: item.Name, + Code: item.Code, + Status: item.Status, + PlatformCode: item.PlatformCode, + Price: item.Price, + LongPrice: item.LongPrice, } + + if dict, ok := dictDatas[respItem.PlatformCode]; ok { + respItem.PlatformName = dict.DictLabel + } + + switch item.PlatformCode { + case global.SmsPlatformDaisysms: + if config, ok := mapConfigs["number_premium_daisysms"]; ok { + premium, err := decimal.NewFromString(config.ConfigValue) + + if err != nil || premium.IsZero() { + e.Log.Errorf("浮动百分比为0,或者是转换错误 %s", config.ConfigValue) + } else { + respItem.Price = respItem.Price.Mul(decimal.NewFromInt(100).Add(premium).Div(decimal.NewFromInt(100))).Truncate(2) + } + } + if config, ok := mapConfigs["long_number_premium_daisysms"]; ok { + premium, err := decimal.NewFromString(config.ConfigValue) + + if err != nil || premium.IsZero() { + e.Log.Errorf("浮动百分比为0,或者是转换错误 %s", config.ConfigValue) + } else { + respItem.LongPrice = respItem.LongPrice.Mul(decimal.NewFromInt(100).Add(premium).Div(decimal.NewFromInt(100))).Truncate(2) + } + } + case global.SmsPlatformTextVerified: + if config, ok := mapConfigs["number_premium_textverified"]; ok { + premium, err := decimal.NewFromString(config.ConfigValue) + + if err != nil || premium.IsZero() { + e.Log.Errorf("浮动百分比为0,或者是转换错误 %s", config.ConfigValue) + } else { + respItem.Price = respItem.Price.Mul(decimal.NewFromInt(100).Add(premium).Div(decimal.NewFromInt(100))).Truncate(2) + } + } + if config, ok := mapConfigs["long_number_premium_textverified"]; ok { + premium, err := decimal.NewFromString(config.ConfigValue) + + if err != nil || premium.IsZero() { + e.Log.Errorf("浮动百分比为0,或者是转换错误 %s", config.ConfigValue) + } else { + respItem.LongPrice = respItem.LongPrice.Mul(decimal.NewFromInt(100).Add(premium).Div(decimal.NewFromInt(100))).Truncate(2) + } + } + } + *resp = append(*resp, respItem) } return nil @@ -66,13 +187,17 @@ func (e SmsServices) GetList(resp *[]dto.SmsServicesGetListResp) error { func (e *SmsServices) GetPage(c *dto.SmsServicesGetPageReq, p *actions.DataPermission, list *[]models.SmsServices, count *int64) error { var err error var data models.SmsServices - - err = e.Orm.Model(&data). + query := e.Orm.Model(&data). Scopes( cDto.MakeCondition(c.GetNeedSearch()), cDto.Paginate(c.GetPageSize(), c.GetPageIndex()), actions.Permission(data.TableName(), p), - ). + ) + + if c.Status > 0 { + query = query.Where("status = ?", c.Status) + } + err = query. Find(list).Limit(-1).Offset(-1). Count(count).Error if err != nil { @@ -154,12 +279,30 @@ func (e *SmsServices) Remove(d *dto.SmsServicesDeleteReq, p *actions.DataPermiss return nil } -func (e *SmsServices) GetByCode(code string) (models.SmsServices, error) { +func (e *SmsServices) GetByCode(platformCode, code string) (models.SmsServices, error) { var data models.SmsServices - if err := e.Orm.Model(data).Where("code =?", code).First(&data).Error; err != nil { + if err := e.Orm.Model(data).Where("platform_code =? and code =?", platformCode, code).First(&data).Error; err != nil { return data, err } return data, nil } + +// 同步第三方价格 +func (e *SmsServices) SyncPrices() error { + var datas []models.SmsServices + + if err := e.Orm.Model(&models.SmsServices{}).Where("platform_code =?", global.SmsPlatformTextVerified).Find(&datas).Error; err != nil { + return err + } + + daisysmsService := SmsDaisysms{Service: e.Service} + textVerifiedService := SmsTextVerified{Service: e.Service} + + //daisysms 同步价格 + daisysmsService.SyncPrices() + //textVerified 同步价格 + textVerifiedService.SyncPrices() + return nil +} diff --git a/app/admin/service/sms_text_verified.go b/app/admin/service/sms_text_verified.go new file mode 100644 index 0000000..22c6b63 --- /dev/null +++ b/app/admin/service/sms_text_verified.go @@ -0,0 +1,855 @@ +package service + +import ( + "errors" + "fmt" + "go-admin/app/admin/models" + "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" + "go-admin/utils/utility" + "net/http" + "net/url" + "path" + "regexp" + "strings" + "time" + + "github.com/bytedance/sonic" + "github.com/go-admin-team/go-admin-core/sdk/service" + "github.com/go-redis/redis/v8" + "github.com/shopspring/decimal" + "gorm.io/gorm" +) + +type SmsTextVerified struct { + service.Service +} + +// 获取收到的验证码 +// messageId 短效或长效号码的ID +// typ 0-短效 1-长效 +func (e SmsTextVerified) GetCode(messageId string, typ int, unixTime int64) (string, int) { + reservationType := "" + + if typ == 0 { + reservationType = "verification" + } else { + reservationType = "renewable" + } + url := fmt.Sprintf(getSmsCode, messageId, reservationType) + client, code := e.GetTextVerifiedAuthClient() + + if code != http.StatusOK { + e.Log.Errorf("获取授权请求失败,status %d", code) + return "", code + } + + var parsedCode string + resp := dto.TextVerifiedSmsResp{} + + _, err := client.Get(url, nil, &resp) + + if err != nil { + e.Log.Errorf("获取验证码失败, error: %v", err) + return "", statuscode.ServerError + } + + if len(resp.Data) > 0 { + for _, v := range resp.Data { + // 2. 解析时间字符串 + // time.RFC3339Nano 是一个预定义的格式常量, + // 它能够解析包含纳秒和时区信息的 ISO 8601 字符串。 + t, err := time.Parse(time.RFC3339Nano, v.CreatedAt) + if err != nil { + fmt.Println("时间解析失败:", err) + } + + // 3. 将解析后的 time.Time 对象转换为 Unix 时间戳 + // t.Unix() 返回以秒为单位的整数时间戳 + unixTimestamp := t.Unix() + + if unixTimestamp >= unixTime { + parsedCode = v.ParsedCode + break + } + } + } + + return parsedCode, http.StatusOK +} + +const ( + loginUrl = "/api/pub/v2/auth" + getAreas = "/api/pub/v2/area-codes" + getServices = "/api/pub/v2/services" + createRental = "/api/pub/v2/reservations/rental" //长效收码 + createVerification = "/api/pub/v2/verifications" //单次收码 + cancelVerification = "/api/pub/v2/verifications/%s/cancel" //取消收码 + cancelRental = "/api/pub/v2/reservations/rental/renewable/%s/refund" //取消长效号码 + wakeUp = "/api/pub/v2/wake-requests" //唤醒号码 + rentalPrice = "/api/pub/v2/pricing/rentals" //长效收码价格 + verificationPrice = "/api/pub/v2/pricing/verifications" //单次收码价格 + rentalDetail = "/api/pub/v2/reservations/rental/renewable/%s" //长效号码详情 + renewRental = "/api/pub/v2/reservations/rental/renewable/%s/renew" //长效续期 + updateRentalRenewStatus = "/api/pub/v2/reservations/rental/renewable/%s" // 更改续租状态 + getSmsCode = "/api/pub/v2/sms?reservationId=%s&reservationType=%s" //获取短信验证码 +) + +var idPattern = regexp.MustCompile(`^[a-zA-Z0-9_]{28}$`) + +// 获取授权失败 +var ErrUnAuth = errors.New("未授权,请检查配置的账号和密码") +var ErrOutOfStockOrUnavailable = errors.New("缺货或服务不可用") + +// Login 登录 +func (e *SmsTextVerified) Login() (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, + } + _, err1 := client.Post(loginUrl, 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秒过期 + if err := redishelper.DefaultRedis.SetStringExpire(global.TextVerifiedToken, token, time.Duration(resp.ExpiresIn)*time.Second); err != nil { + e.Log.Errorf("TextVerified登录失败,缓存Token失败 error: %v", err) + } + } + + return token, nil +} + +// 获取token +func (e *SmsTextVerified) GetToken() (string, error) { + token, err := redishelper.DefaultRedis.GetString(global.TextVerifiedToken) + if err != nil && errors.Is(err, redis.Nil) { + // token不存在,重新登录获取 + return e.Login() + } + + if token == "" { + return e.Login() + } + + return token, nil +} + +// 获取区域 +func (e *SmsTextVerified) GetAreas() ([]string, error) { + + // 这里可以实现获取地区的逻辑 + // 例如从数据库或配置文件中读取地区信息 + areas := []string{"Area1", "Area2", "Area3"} // 示例数据 + return areas, nil +} + +// 获取服务列表 +func (e *SmsTextVerified) GetServices() ([]dto.TextVerifiedServeResp, error) { + client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.SmsTextVerified.Url, nil) + headers, err := e.GetAuthHeader() + if err != nil { + return nil, err + } + resp := []dto.TextVerifiedServeResp{} + url := fmt.Sprintf("%s?numberType=mobile&reservationType=%s", getServices, "verification") + _, err1 := client.Get(url, headers, &resp) + if err1 != nil { + e.Log.Errorf("TextVerified获取服务列表失败 error: %v", err1) + return nil, err1 + } + + return resp, nil +} + +// 同步服务列表 +func (e *SmsTextVerified) SyncServices() error { + services, err := e.GetServices() + if err != nil { + e.Log.Errorf("获取服务列表失败: %v", err) + return err + } + + // 提取所有服务的 Capability 作为查询条件 + capabilities := make([]string, 0, len(services)) + for _, service := range services { + if strings.ToLower(service.Capability) != "sms" { + continue + } + + if utility.ContainsString(capabilities, service.ServiceName) { + continue + } + + capabilities = append(capabilities, service.ServiceName) + } + + // 批量查询已存在的服务 + var existingServices []models.SmsServices + err = e.Orm.Model(&models.SmsServices{}). + Where("platform_code = ? AND code IN ?", global.SmsPlatformTextVerified, capabilities). + Find(&existingServices).Error + if err != nil { + e.Log.Errorf("批量查询服务失败: %v", err) + return err + } + + // 创建现有服务的 Code 集合,便于快速查找 + existingServiceCodes := make(map[string]struct{}, len(existingServices)) + for _, svc := range existingServices { + existingServiceCodes[svc.Code] = struct{}{} + } + + // 筛选出需要新增的服务 + entitys := make([]models.SmsServices, 0) + for _, service := range services { + if _, exists := existingServiceCodes[service.ServiceName]; !exists && strings.ToLower(service.Capability) == "sms" { + entity := models.SmsServices{ + Name: service.ServiceName, + Code: service.ServiceName, + PlatformCode: global.SmsPlatformTextVerified, + ExpirationMinutes: 3, + Status: 2, + } + entitys = append(entitys, entity) + } + } + + // 批量创建新服务 + if len(entitys) > 0 { + err = e.Orm.Transaction(func(tx *gorm.DB) error { + if err := tx.CreateInBatches(&entitys, 500).Error; err != nil { + e.Log.Errorf("同步服务列表失败: %v", err) + return err + } + return nil + }) + if err != nil { + return err + } + } + + return nil +} + +// 同步价格 +func (e *SmsTextVerified) SyncPrices() error { + var services []models.SmsServices + var entity models.SmsServices + + if err := e.Orm.Model(&entity).Where("platform_code = ? and status =1", global.SmsPlatformTextVerified).Find(&services).Error; err != nil { + return err + } + + for _, v := range services { + params := map[string]interface{}{} + + price, code := e.GetPrice(0, v.Code) + + if code == http.StatusOK { + params["price"] = price + } + + time.Sleep(time.Microsecond * 500) // 避免请求过快被限制 + longPrice, code := e.GetPrice(1, v.Code) + if code == http.StatusOK { + params["long_price"] = longPrice + } + + if len(params) > 0 { + if err := e.Orm.Model(&entity).Where("platform_code = ? and code = ?", global.SmsPlatformTextVerified, v.Code).Updates(params).Error; err != nil { + e.Log.Errorf("同步服务价格失败: %v", err) + } + } + } + + return nil +} + +// 号码租赁 +// getType 0-短效 1-长效 +// service 服务code +// maxPrice 最大价格 +// period 时长(月=30天) +func (e SmsTextVerified) GetNumberForApi(typ int, serviceCode string, price decimal.Decimal, period int) (dto.SmsPhoneGetPhoneResp, int) { + //这个平台的所有号码都是美国号码 默认国家代码都是 +1 + var err error + var createResp dto.TextVerifiedResp + var result dto.SmsPhoneGetPhoneResp + switch typ { + case 0: + var code int + createResp, code = e.CreateVerification(serviceCode) + + if code == statuscode.Success && createResp.Href != "" { + bytes, err := e.doRequest(&createResp) + + if err != nil { + e.Log.Errorf("短效号码 获取号码失败:%v", e) + return result, 0 + } + + resp := dto.VerificationDTO{} + + if err := sonic.Unmarshal(bytes, &resp); err != nil { + e.Log.Errorf("短效号码 获取号码失败:%v", e) + return result, 0 + } + + result.Id = resp.ID + result.Phone = "1" + resp.Number + result.EndAt = &resp.EndsAt + + return result, statuscode.Success + } else if code == statuscode.Success && createResp.Href == "" { + e.Log.Errorf("获取号码没有 返回值") + return result, statuscode.ServerError + } else { + e.Log.Errorf("获取号码失败:%v", err) + return result, statuscode.ServerError + } + case 1: + createResp, err = e.CreateRental(serviceCode) + + if err == nil && createResp.Href != "" { + saleId := getIdByUrl(createResp.Href) + bytes, err := e.doRequest(&createResp) + + if err != nil { + e.Log.Errorf("通过销售id [%s] 获取号码失败:%v", saleId, err) + return result, statuscode.ServerError + } + + resp := dto.SaleResponse{} + if err := sonic.Unmarshal(bytes, &resp); err != nil { + e.Log.Errorf("反序列化失败:%v", err) + + return result, statuscode.ServerError + } + + if len(resp.Reservations) > 0 { + for _, v := range resp.Reservations { + if v.ID != "" { + result.Id = v.ID + continue + } + } + + if result.Id == "" { + e.Log.Errorf("获取长效号码失败,没有返回值") + return result, statuscode.ServerError + } + + detail, code := e.GetRentalDetail(result.Id) + + if code != statuscode.Success { + return result, code + } + + result.Phone = "1" + detail.Number + endTime := resp.UpdatedAt.AddDate(0, 0, 30) + result.EndAt = &(endTime) // 30天后过期 + + return result, statuscode.Success + } + + } else if err == nil && createResp.Href == "" { + e.Log.Errorf("获取长效号码失败,没有返回值") + return result, statuscode.ServerError + } else { + if err == ErrOutOfStockOrUnavailable { + return result, statuscode.SmsOutOfStockOrUnavailable + } + e.Log.Errorf("获取长效号码失败:%v", err) + return result, statuscode.ServerError + } + + default: + return result, statuscode.SmsPlatformUnavailable + } + + if err != nil { + return result, statuscode.ServerError + } + + return result, statuscode.Success +} + +// 执行查询请求 +func (e *SmsTextVerified) doRequest(resp *dto.TextVerifiedResp) ([]byte, error) { + bytes := []byte{} + if resp.Href == "" { + return bytes, errors.New("成功请求没有返回url") + } + + header, err := e.GetAuthHeader() + + if err != nil { + return bytes, fmt.Errorf("获取授权失败:%v", err) + } + + client := httphelper.NewHTTPClient(10*time.Second, "", header) + + _, err = client.DoRequest(resp.Method, resp.Href, nil, nil, nil, &bytes) + + if err != nil { + return bytes, fmt.Errorf("请求失败:%v", err) + } + + return bytes, nil +} + +// 长号码租赁 +func (e *SmsTextVerified) CreateRental(serviceCode string) (dto.TextVerifiedResp, error) { + req := dto.TextVerifiedCreateRewalReq{ + ServiceName: serviceCode, + Capability: "sms", + NumberType: "mobile", + IsRenewable: true, + AlwaysOn: false, + Duration: "thirtyDay", + } + + resp := dto.TextVerifiedResp{} + + client, code := e.GetTextVerifiedAuthClient() + + if code != statuscode.Success { + e.Log.Errorf("获取头信息失败 error: %d", code) + return resp, ErrUnAuth + } + + headers := map[string]string{ + "Content-Type": "application/json", + } + status, err := client.Post(createRental, req, headers, &resp) + + // 201 成功 + if status == http.StatusCreated || resp.Href != "" { + return resp, nil + } else if err != nil { + if strings.Contains(err.Error(), "Out of stock or unavailable") { + e.Log.Errorf("缺货或服务不可用 %v", err) + return resp, ErrOutOfStockOrUnavailable + } + e.Log.Errorf("短信平台不可用 error: %v", err) + return resp, err + } + + return resp, fmt.Errorf("短信平台不可用 status: %v, error: %v", status, err) +} + +// 获取长效号码详情 +func (e *SmsTextVerified) GetRentalDetail(id string) (dto.VerificationRentalDetailResp, int) { + result := dto.VerificationRentalDetailResp{} + + client, code := e.GetTextVerifiedAuthClient() + if code != statuscode.Success { + return result, code + } + + _, err := client.Get(fmt.Sprintf(rentalDetail, id), nil, &result) + + if err != nil { + e.Log.Errorf("查询长效详情失败 id:%s error: %v", id, err) + return result, statuscode.ServerError + } + + return result, statuscode.Success +} + +// 唤醒号码 +// returns activationId,messageId, code +func (e SmsTextVerified) getExtraActivation(id string) (string, string, int) { + client, code := e.GetTextVerifiedAuthClient() + + if code != statuscode.Success { + return "", "", code + } + + req := dto.TextVerifiedWakeUpReq{ + RevervationId: id, + } + + resp := dto.TextVerifiedResp{} + headers := map[string]string{ + "Content-Type": "application/json", + } + status, err := client.Post(wakeUp, req, headers, &resp) + + if err != nil { + e.Log.Errorf("唤醒号码失败 id:%s error: %v", id, err) + return "", "", statuscode.ServerError + } else if status == http.StatusCreated || status == http.StatusOK { + if resp.Method != "" && resp.Href != "" { + bytes, err := e.doRequest(&resp) + + if err != nil { + e.Log.Errorf("唤醒号码失败 id:%s error: %v", id, err) + return "", "", statuscode.ServerError + } + + detailResp := dto.TextVerifiedWakeUpResp{} + + if err := sonic.Unmarshal(bytes, &detailResp); err != nil { + e.Log.Errorf("唤醒号码反序列化失败 id:%s error: %v", id, err) + return "", "", statuscode.ServerError + } + + return detailResp.ReservationId, detailResp.Id, statuscode.Success + } else { + e.Log.Errorf("唤醒号码失败 id:%s error: %v", id, err) + return "", "", statuscode.ServerError + } + } + + return "", "", statuscode.ServerError +} + +// 获取价格 +// getType 0-短效 1-长效 +// returns decimal.Decimal(单价), int(状态code) +func (e *SmsTextVerified) GetPrice(typ int, serviceName string) (decimal.Decimal, int) { + switch typ { + case 1: + return e.GetRentalPrice(serviceName) + case 0: + return e.GetVerificationPrice(serviceName) + default: + return decimal.Zero, statuscode.SmsInvalidType + } +} + +// 获取长租价格 +func (e *SmsTextVerified) GetRentalPrice(serviceName string) (decimal.Decimal, int) { + req := dto.TextVerifiedPriceReq{ + ServiceName: serviceName, + Capability: "sms", + NumberType: "mobile", + AreaCode: false, + Duration: "thirtyDay", + } + + client, code := e.GetTextVerifiedAuthClient() + + if code != statuscode.Success { + e.Log.Errorf("获取授权请求失败,status %d", code) + return decimal.Zero, code + } + + resp := dto.TextVerifiedPriceResp{} + headers := map[string]string{ + "Content-Type": "application/json", + } + + _, err := client.Post(rentalPrice, req, headers, &resp) + + if err != nil { + e.Log.Errorf("获取价格失败, error: %v", err) + return decimal.Zero, statuscode.ServerError + } + + return resp.Price, statuscode.Success +} + +// 获取单次验证码价格 +func (e *SmsTextVerified) GetVerificationPrice(sericeName string) (decimal.Decimal, int) { + params := map[string]interface{}{ + "serviceName": sericeName, + "areaCode": false, + "carrier": false, + "capability": "sms", + "numberType": "mobile", + } + + client, code := e.GetTextVerifiedAuthClient() + + if code != statuscode.Success { + e.Log.Errorf("获取授权请求失败,status %d", code) + return decimal.Zero, code + } + + resp := dto.TextVerifiedPriceResp{} + headers := map[string]string{ + "Content-Type": "application/json", + } + _, err := client.Post(verificationPrice, params, headers, &resp) + + if err != nil { + e.Log.Errorf("获取价格失败, error: %v", err) + return decimal.Zero, statuscode.ServerError + } + + return resp.Price, statuscode.Success +} + +// 单次接收 +func (e *SmsTextVerified) CreateVerification(serviceCode string) (dto.TextVerifiedResp, int) { + req := dto.TextVerifiedCreateRewalReq{ + ServiceName: serviceCode, + Capability: "sms", + } + + resp := dto.TextVerifiedResp{} + client, code := e.GetTextVerifiedAuthClient() + + if code != statuscode.Success { + return resp, code + } + headers := map[string]string{ + "Content-Type": "application/json", + } + status, err := client.Post(createVerification, req, headers, &resp) + + // 201 成功 + if status == http.StatusCreated { + return resp, statuscode.Success + } + + if err != nil { + e.Log.Errorf("TextVerified创建单次收码失败 error: %v", err) + return resp, statuscode.ServerError + } + + return resp, statuscode.Success +} + +// 取消验证码 +// typ 0-短效 1-长效 +func (e SmsTextVerified) CancelRental(id string, typ int) int { + client, code := e.GetTextVerifiedAuthClient() + + if code != statuscode.Success { + return code + } + + var url string + + if typ == 0 { + url = fmt.Sprintf(cancelVerification, id) + } else { + url = fmt.Sprintf(cancelRental, id) + } + + headers := map[string]string{ + "Content-Type": "application/json", + } + _, code, err := client.PostRaw(url, headers) + + if err != nil { + if strings.Contains(err.Error(), "Self service refund not permitted for this rental") { + return statuscode.SmsRentalRefundNotPermitted + } + + e.Log.Errorf("TextVerified取消验证码失败 error: %v", err) + return statuscode.ServerError + } + + if code != http.StatusOK { + e.Log.Errorf("TextVerified取消验证码失败,返回状态码 %d", code) + } + + return statuscode.Success +} + +func (e *SmsTextVerified) GetTextVerifiedAuthClient() (*httphelper.HTTPClient, int) { + header, err := e.GetAuthHeader() + + if err != nil { + e.Log.Errorf("取消验证码获取token失败 error: %v", err) + return nil, statuscode.ServerError + } + client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.SmsTextVerified.Url, header) + + return client, statuscode.Success +} + +// TextVerifiedWebHook TextVerified短信发送记录回调 +func (e *SmsTextVerified) TextVerifiedWebHook(req *dto.TextVerifiedWebHookReq) error { + entity := &models.SmsPhone{} + + switch req.Event { + //续订回调 + case "v2.rental.billingcycle.renewed": + e.Log.Infof("续订回调 %v", req) + data := dto.TextVerifiedWebHookRenewedReq{} + val, err := sonic.MarshalString(req.Data) + + if err != nil { + e.Log.Errorf("数据类型错误 %v", req) + return fmt.Errorf("数据类型错误 %s", req.Data) + } + + err = sonic.UnmarshalString(val, &data) + + if err != nil { + e.Log.Errorf("数据类型错误 %v", req) + return fmt.Errorf("数据类型错误 %s", req.Data) + } + //过期回调 + case "v2.rental.billingcycle.expired": + e.Log.Infof("过期 %v", req) + // data := dto.TextVerifiedWebHookBillingCycleExpiredReq{} + // val, err := sonic.MarshalString(req.Data) + + // if err != nil { + // e.Log.Errorf("数据类型错误 %v", req) + // return fmt.Errorf("数据类型错误 %s", req.Data) + // } + + // err = sonic.UnmarshalString(val, &data) + + // if err != nil { + // e.Log.Errorf("数据类型错误 %v", req) + // return fmt.Errorf("数据类型错误 %s", req.Data) + // } + + // if err := e.Orm.Model(&entity).Where("new_activation_id =? and status ='1'", data.RevervationId).Updates(map[string]interface{}{"status": 3}).Error; err != nil { + // return err + // } + //收到短信回调 + case "v2.sms.received": + e.Log.Infof("收到短信回调 %v", req) + data := dto.TextVerifiedWebHookSmsReceivedReq{} + + val, err := sonic.MarshalString(req.Data) + + if err != nil { + e.Log.Errorf("数据类型错误 %v", req) + return fmt.Errorf("数据类型错误 %s", req.Data) + } + + err = sonic.UnmarshalString(val, &data) + + if err != nil { + e.Log.Errorf("数据类型错误 %v", req) + return fmt.Errorf("数据类型错误 %s", req.Data) + } + + if err := e.Orm.Model(&entity).Where("new_activation_id =? and status =1", data.ReservationId).Updates(map[string]interface{}{"status": "2", "actived": 2, "code": data.ParsedCode}).Error; err != nil { + e.Log.Errorf("数据类型错误 %v", req) + return fmt.Errorf("数据类型错误 %s", req.Data) + } + //预定订单完成回调 + case "v2.rental.backorder.fulfilled": + e.Log.Infof("预定订单完成回调 %v", req) + data := dto.TextVerifiedWebHookFulfilledReq{} + val, err := sonic.MarshalString(req.Data) + + if err != nil { + e.Log.Errorf("数据类型错误 %v", req) + return fmt.Errorf("数据类型错误 %s", req.Data) + } + + err = sonic.UnmarshalString(val, &data) + + if err != nil { + e.Log.Errorf("数据类型错误 %v", req) + return fmt.Errorf("数据类型错误 %s", req.Data) + } + + // if err:=e.Orm.Model(&entity).Where("new_activation_id =? ",data.) + // 预定订单创建 + case "v2.reservation.created": + // e.Log.Infof("预定订单创建 %v", req) + // data := dto.TextVerifiedWebHookReservationCreatedReq{} + // val, err := sonic.MarshalString(req.Data) + + // if err != nil { + // e.Log.Errorf("数据类型错误 %v", req) + // return fmt.Errorf("数据类型错误 %s", req.Data) + // } + + // err = sonic.UnmarshalString(val, &data) + + // if err != nil { + // e.Log.Errorf("数据类型错误 %v", req) + // return fmt.Errorf("数据类型错误 %s", req.Data) + // } + default: + e.Log.Errorf("未知的事件类型 %s", req.Event) + return nil + } + + return nil +} + +// 续期 +// typ 0-短效 1-长效 +func (e *SmsTextVerified) Renew(activationId string, status bool) int { + url := fmt.Sprintf(updateRentalRenewStatus, activationId) + + client, code := e.GetTextVerifiedAuthClient() + + if code != statuscode.Success { + e.Log.Errorf("获取长效续期失败 %d", code) + return statuscode.ServerError + } + + params := map[string]interface{}{ + "includeForRenewal": status, + } + + headers := map[string]string{ + "Content-Type": "application/json", + } + mapData := map[string]interface{}{} + statuCode, err := client.Post(url, params, headers, &mapData) + + if err != nil { + e.Log.Errorf("修改长效续期状态失败 %v", err) + } + + if statuCode == http.StatusOK { + return statuscode.Success + } else { + e.Log.Errorf("修改长效续期状态失败 %d", statuCode) + return statuscode.ServerError + } +} + +// 获取授权header头 +func (e *SmsTextVerified) GetAuthHeader() (map[string]string, error) { + token, err := e.GetToken() + if err != nil { + return nil, err + } + headers := map[string]string{ + "Authorization": "Bearer " + token, + } + return headers, nil +} + +// 获取id 根据url (https://www.textverified.com/api/pub/v2/sales/{id}}") +func getIdByUrl(URLString string) string { + parsedNewURL, _ := url.Parse(URLString) + newID := path.Base(parsedNewURL.Path) + + if isValidID(newID) { + return newID + } else { + return "" + } +} + +func isValidID(s string) bool { + return idPattern.MatchString(s) +} diff --git a/app/admin/service/sms_text_verified_test.go b/app/admin/service/sms_text_verified_test.go new file mode 100644 index 0000000..0f1f111 --- /dev/null +++ b/app/admin/service/sms_text_verified_test.go @@ -0,0 +1,53 @@ +package service + +import ( + "testing" + + "github.com/go-admin-team/go-admin-core/logger" +) + +func TestSmsTextVerifiedLogin(t *testing.T) { + db := initSetting() + + s := SmsTextVerified{} + s.Orm = db + s.Log = logger.NewHelper(logger.DefaultLogger) + + token, err := s.Login() + if err != nil { + t.Errorf("Login failed: %v", err) + } else { + t.Logf("Login succeeded, token: %s", token) + } +} + +func TestSmsTextVerifiedGetServices(t *testing.T) { + db := initSetting() + + s := SmsTextVerified{} + s.Orm = db + s.Log = logger.NewHelper(logger.DefaultLogger) + + // Now, test GetServices with the valid token + servicesResp, err := s.GetServices() + if err != nil { + t.Errorf("GetServices failed: %v", err) + } else { + t.Logf("GetServices succeeded, services: %+v", servicesResp) + } +} + +func TestSyncTextVerifiedServices(t *testing.T) { + db := initSetting() + + s := SmsTextVerified{} + s.Orm = db + s.Log = logger.NewHelper(logger.DefaultLogger) + + err := s.SyncServices() + if err != nil { + t.Errorf("SyncTextVerifiedServices failed: %v", err) + } else { + t.Log("SyncTextVerifiedServices succeeded") + } +} diff --git a/app/admin/service/sys_config.go b/app/admin/service/sys_config.go index 6f15662..f040680 100644 --- a/app/admin/service/sys_config.go +++ b/app/admin/service/sys_config.go @@ -175,6 +175,31 @@ func (e *SysConfig) GetWithKey(c *dto.SysConfigByKeyReq, resp *dto.GetSysConfigB return nil } +func (e *SysConfig) GetWithKeyForList(keys []string, list *[]dto.GetSysConfigByKEYForServiceResp) error { + if err := e.Orm.Model(&models.SysConfig{}).Where("config_key in ?", keys). + Find(list).Error; err != nil { + e.Log.Errorf("Service GetSysConfigByKey error:%s", err) + return err + } + return nil +} + +func (e *SysConfig) GetWithKeyForMap(keys []string) (map[string]dto.GetSysConfigByKEYForServiceResp, error) { + listData := make([]dto.GetSysConfigByKEYForServiceResp, 0) + err := e.GetWithKeyForList(keys, &listData) + result := make(map[string]dto.GetSysConfigByKEYForServiceResp) + + if err != nil { + return result, err + } + + for _, v := range listData { + result[v.ConfigKey] = v + } + + return result, nil +} + func (e *SysConfig) GetWithKeyList(c *dto.SysConfigGetToSysAppReq, list *[]models.SysConfig) error { var err error err = e.Orm. diff --git a/app/admin/service/sys_dict_data.go b/app/admin/service/sys_dict_data.go index 553ad4c..762f439 100644 --- a/app/admin/service/sys_dict_data.go +++ b/app/admin/service/sys_dict_data.go @@ -15,6 +15,33 @@ type SysDictData struct { service.Service } +func (e *SysDictData) GetMapByType(typ string) (map[string]models.SysDictData, error) { + datas, err := e.GetByType(typ) + result := make(map[string]models.SysDictData) + + if err != nil { + return nil, err + } + + for _, data := range datas { + result[data.DictValue] = data + } + + return result, nil +} + +// 根据 type获取字典数据 +func (e SysDictData) GetByType(typ string) ([]models.SysDictData, error) { + var datas []models.SysDictData + + if err := e.Orm.Model(&models.SysDictData{}).Where("dict_type = ?", typ).Find(&datas).Error; err != nil { + e.Log.Errorf("根据type获取字典数据失败,\r\n失败信息 %s", err.Error()) + return nil, err + } + + return datas, nil +} + // GetPage 获取列表 func (e *SysDictData) GetPage(c *dto.SysDictDataGetPageReq, list *[]models.SysDictData, count *int64) error { var err error diff --git a/app/admin/service/sys_user.go b/app/admin/service/sys_user.go index 8ad4b9f..191bb3f 100644 --- a/app/admin/service/sys_user.go +++ b/app/admin/service/sys_user.go @@ -50,6 +50,9 @@ func (e SysUser) Register(req *dto.RegisterReq) (dto.SysUserRegisterResp, int) { } data.RoleId = role.RoleId + memberApiService := MemberApi{} + memberApiService.Log = e.Log + var memberApi models.MemberApi err := e.Orm.Transaction(func(tx *gorm.DB) error { if err1 := tx.Create(&data).Error; err1 != nil { @@ -67,6 +70,13 @@ func (e SysUser) Register(req *dto.RegisterReq) (dto.SysUserRegisterResp, int) { result.Token = token result.Expire = int(expire.Unix()) + memberApiService.Orm = tx + + if err1 := memberApiService.CreateApi(data.UserId, &memberApi, 0); err1 != nil { + e.Log.Errorf("创建用户失败 %v", err1) + return err1 + } + return nil }) @@ -170,11 +180,27 @@ func (e *SysUser) Insert(c *dto.SysUserInsertReq) error { return err } c.Generate(&data) - err = e.Orm.Create(&data).Error - if err != nil { - e.Log.Errorf("db error: %s", err) - return err - } + + memberApiService := MemberApi{} + memberApiService.Log = e.Log + + err = e.Orm.Transaction(func(tx *gorm.DB) error { + err = tx.Create(&data).Error + if err != nil { + e.Log.Errorf("db error: %s", err) + return err + } + + memberApiService.Orm = tx + + if err1 := memberApiService.CreateApi(data.UserId, &models.MemberApi{}, 0); err1 != nil { + e.Log.Errorf("创建用户API失败, %s", err1) + return err1 + } + + return nil + }) + return nil } diff --git a/app/jobs/examples.go b/app/jobs/examples.go index 685b543..ecf58eb 100644 --- a/app/jobs/examples.go +++ b/app/jobs/examples.go @@ -19,6 +19,7 @@ func InitJob() { "SmsJob": SmsJob{}, //短信定时查询验证码 "SmsRenewalJob": SmsRenewalJob{}, //短信定时自动续期 "AutoDeleteJob": AutoDeleteJob{}, //定时删除任务 + "SmsPriceJob": SmsPriceJob{}, // 短信价格定时同步 // ... } } diff --git a/app/jobs/sms_job.go b/app/jobs/sms_job.go index 089fb02..0f46c93 100644 --- a/app/jobs/sms_job.go +++ b/app/jobs/sms_job.go @@ -7,6 +7,7 @@ import ( ) type SmsJob struct{} +type SmsPriceJob struct{} // 定时查询结果 func (j SmsJob) Exec(args interface{}) error { @@ -16,3 +17,17 @@ func (j SmsJob) Exec(args interface{}) error { return phoneService.SyncCodes() } + +// 定时同步价格 +func (j SmsPriceJob) Exec(args interface{}) error { + smsService := service.SmsServices{} + smsService.Orm = GetDb() + smsService.Log = logger.NewHelper(logger.DefaultLogger) + + // 同步价格 + if err := smsService.SyncPrices(); err != nil { + return err + } + + return nil +} diff --git a/app/jobs/sms_job_test.go b/app/jobs/sms_job_test.go index 60a75e4..da3d12f 100644 --- a/app/jobs/sms_job_test.go +++ b/app/jobs/sms_job_test.go @@ -25,3 +25,13 @@ func TestSmsRenew(t *testing.T) { t.Error(err) } } + +func TestSmsPriceJob(t *testing.T) { + initSetting() + config.ExtConfig.DaisysmsUrl = "https://daisysms.com/stubs/handler_api.php" + job := SmsPriceJob{} + + if err := job.Exec(nil); err != nil { + t.Error(err) + } +} diff --git a/app/jobs/trx_job.go b/app/jobs/trx_job.go index 4e0234d..2ae4cfe 100644 --- a/app/jobs/trx_job.go +++ b/app/jobs/trx_job.go @@ -5,8 +5,9 @@ import ( "go-admin/app/admin/service" "go-admin/app/admin/service/dto" "go-admin/config" + "go-admin/utils/redishelper" "go-admin/utils/utility" - "io/ioutil" + "io" "net/http" "time" @@ -26,6 +27,19 @@ const ( // trx 链上支付定时查询 func (j TrxPaymentJob) Exec(arg interface{}) error { + key := fmt.Sprintf("pre_order:%s", "*") + keys, err := redishelper.DefaultRedis.ScanKeys(key) + + if err != nil { + logger.Error("查询redis key失败", err) + return nil + } + + if len(keys) == 0 { + logger.Info("没有待处理订单") + return nil + } + configService := service.SysConfig{} configService.Orm = GetDb() req := dto.SysConfigByKeyReq{} @@ -82,19 +96,31 @@ func (j TrxPaymentJob) Exec(arg interface{}) error { // GetTRC20Transfers 获取指定 TRC20 代币的交易记录 func GetTRC20Transfers(contractAddress, accountAddress string, minTimestamp, maxTimestamp int64) ([]dto.TRC20Transfer, error) { url := fmt.Sprintf("%s/v1/accounts/%s/transactions/trc20?contract_address=%s", config.ExtConfig.TrxGridUrl, accountAddress, contractAddress) + if minTimestamp > 0 { url += fmt.Sprintf("&min_timestamp=%d", minTimestamp) } if maxTimestamp > 0 { url += fmt.Sprintf("&max_timestamp=%d", maxTimestamp) } - resp, err := http.Get(url) + // logger.Info("查询地址:", url) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + // 设置请求头(包含 TronGrid API Key) + req.Header.Set("Accept", "*/*") + req.Header.Set("TRON-PRO-API-KEY", config.ExtConfig.TronApiKey) // 从配置读取 API Key + + client := &http.Client{} + resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to send request: %v", err) } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %v", err) } @@ -102,6 +128,7 @@ func GetTRC20Transfers(contractAddress, accountAddress string, minTimestamp, max var result struct { Data []dto.TRC20Transfer `json:"data"` } + // logger.Info("查询结果:", string(body)) if err := sonic.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %v", err) } diff --git a/app/jobs/trx_job_test.go b/app/jobs/trx_job_test.go index 5121425..a7f4de4 100644 --- a/app/jobs/trx_job_test.go +++ b/app/jobs/trx_job_test.go @@ -27,6 +27,10 @@ func initSetting() { db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{}) sdk.Runtime.SetDb("default", db) config.ExtConfig.TrxGridUrl = "https://api.trongrid.io" + config.ExtConfig.DaisysmsUrl = "https://daisysms.com/stubs/handler_api.php" + config.ExtConfig.SmsTextVerified.ApiKey = "ZQ0swXnsaPpeGdwa3c7gT9U9I1Oh9WoDHx0amuYovvaHuqd5u6B4NBBUSUBjR" + config.ExtConfig.SmsTextVerified.UserName = "webspan@proton.me" + config.ExtConfig.SmsTextVerified.Url = "https://www.textverified.com" redishelper.InitDefaultRedis("127.0.0.1:6379", "", 4) redishelper.InitLockRedisConn("127.0.0.1:6379", "", "4") } diff --git a/cmd/api/server.go b/cmd/api/server.go index cac641d..b45edb5 100644 --- a/cmd/api/server.go +++ b/cmd/api/server.go @@ -186,9 +186,8 @@ func initRouter() { if config.SslConfig.Enable { r.Use(handler.TlsHandler()) } - //r.Use(middleware.Metrics()) - r.Use(common.Sentinel()). - Use(common.RequestId(pkg.TrafficKey)). + //r.Use(middleware.Metrics()).Use(common.Sentinel()). + r.Use(common.RequestId(pkg.TrafficKey)). Use(api.SetRequestLogger) common.InitMiddleware(r) @@ -218,4 +217,10 @@ func initBusinesses() { if _, err := cliProxyService.GetTrafficInfo(); err != nil { os.Exit(-1) } + + memberApiService := service.MemberApi{} + memberApiService.Orm = cliProxyService.Orm + memberApiService.Log = cliProxyService.Log + + memberApiService.InitApis() } diff --git a/common/global/config_key.go b/common/global/config_key.go index 9d82ab4..76313e2 100644 --- a/common/global/config_key.go +++ b/common/global/config_key.go @@ -2,9 +2,16 @@ package global //ConfigKey const ( - TrafficProxyEffectiveDay = "traffic_proxy_effective_day" //流量代理有效天数 - LongNumberRenewDeductionStandard = "long_number_renew_deduction_standard" //长效号码续费标准 - IPRenewDeductionStandard = "ip_renew_deduction_standard" //长效IP续费kk扣除标准 + //流量代理有效天数 + TrafficProxyEffectiveDay = "traffic_proxy_effective_day" + //短期号码扣除标准 + ShortNumberDeductionStandard = "number_fee_short_term" + //长效号码扣除标准 + LongNumberDeductionStandard = "number_fee_long_term" + //长效号码续费标准 + LongNumberRenewDeductionStandard = "long_number_renew_deduction_standard" + //长效IP续费kk扣除标准 + IPRenewDeductionStandard = "ip_renew_deduction_standard" ProxyExpiryTime = "proxy_expiry_time" //代理过期记录保留时长 SmsExpiryTime = "sms_expiry_time" //接码过期记录保留时长 diff --git a/common/global/sms_services.go b/common/global/sms_services.go new file mode 100644 index 0000000..2f19332 --- /dev/null +++ b/common/global/sms_services.go @@ -0,0 +1,7 @@ +package global + +//短效平台code 字典【sms_platform】 +const ( + SmsPlatformDaisysms = "daisysms" + SmsPlatformTextVerified = "textverified" +) diff --git a/common/global/text_verified.go b/common/global/text_verified.go new file mode 100644 index 0000000..981bdef --- /dev/null +++ b/common/global/text_verified.go @@ -0,0 +1,6 @@ +package global + +const ( + // TextVerified Token + TextVerifiedToken = "TextVerifiedToken" +) diff --git a/common/middleware/auth.go b/common/middleware/auth.go index 6562cff..f729a41 100644 --- a/common/middleware/auth.go +++ b/common/middleware/auth.go @@ -1,13 +1,28 @@ package middleware import ( + "errors" + "fmt" + "strconv" "time" - "github.com/go-admin-team/go-admin-core/sdk/config" - jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" + "go-admin/app/admin/models" "go-admin/common/middleware/handler" + "go-admin/common/rediskey" + "go-admin/common/statuscode" + "go-admin/utils/redishelper" + + "github.com/bytedance/sonic" + "github.com/gin-gonic/gin" + "github.com/go-admin-team/go-admin-core/sdk/config" + "github.com/go-admin-team/go-admin-core/sdk/pkg" + jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" ) +var ErrQueryUserId = errors.New("查询用户失败") +var ErrNoAccount = errors.New("没有API用户") +var ErrApiUnActived = errors.New("API未激活") + // AuthInit jwt验证new func AuthInit() (*jwt.GinJWTMiddleware, error) { timeout := time.Hour @@ -33,4 +48,108 @@ func AuthInit() (*jwt.GinJWTMiddleware, error) { TimeFunc: time.Now, }) -} \ No newline at end of file +} + +// apikey授权认证 +func FrontedAuth(c *gin.Context) { + // 从请求头中获取 token 和 os + apikey := c.GetHeader("x-api-key") + // 如果 token 不存在,返回未登录的状态 + if len(apikey) == 0 { + err := ResponseWithStatus(c, statuscode.Unauthorized) + if err != nil { + return + } + c.Abort() // 停止后续中间件的执行 + return + } + // 验证 token 并获取结果 + key := fmt.Sprintf(rediskey.MemberApiKey, apikey) + val, err := redishelper.DefaultRedis.GetString(key) + + if err != nil || val == "" { + ResponseWithStatus(c, statuscode.Unauthorized) + c.Abort() // 停止后续中间件的执行 + } + + // 将解析后的 token 设置到请求头中 + c.Set("apiKey", apikey) + // 继续处理请求 + c.Next() +} + +// ResponseWithStatus 带状态的响应 +func ResponseWithStatus(ctx *gin.Context, code int, data ...interface{}) error { + resp := statuscode.Response{ + Code: code, + Msg: "un authorized", + } + + // resp.RequestID = pkg.GenerateMsgIDFromContext(ctx) + if len(data) > 0 { + resp.Data = data[0] + } + + switch code { + case 401, 500, 405, 404: + ctx.JSON(code, resp) + default: + ctx.JSON(200, resp) + } + + return nil +} + +// 获取ApiKey用户id +func GetUserIdWithApiKey(ctx *gin.Context) (int, error) { + apikey, ok := ctx.Get("apiKey") + if !ok { + return 0, errors.New("apiKey not found") + } + return strconv.Atoi(apikey.(string)) +} + +// 获取用户id 根据ApiKey +func GetUserIdByApiKey(c *gin.Context) (int, error) { + apiKey, ok := c.Get("apiKey") + if !ok { + return 0, errors.New("apiKey not found") + } + db, err := pkg.GetOrm(c) + + if err != nil { + return 0, err + } + + var userId int + val, err := redishelper.DefaultRedis.GetString(fmt.Sprintf(rediskey.MemberApiKey, apiKey)) + + if err != nil { + return userId, err + } + + if val == "" { + var user models.MemberApi + + if dbErr := db.Model(&user).Where("api_key = ?", apiKey).First(&user).Error; dbErr != nil { + return userId, ErrNoAccount + } + + if user.Status != 1 { + return userId, ErrApiUnActived + } + + userId = user.UserId + } else { + data := models.MemberApi{} + err = sonic.UnmarshalString(val, &data) + + if err != nil { + return userId, err + } + + userId = data.UserId + } + + return userId, nil +} diff --git a/common/rediskey/member_api.go b/common/rediskey/member_api.go new file mode 100644 index 0000000..6328577 --- /dev/null +++ b/common/rediskey/member_api.go @@ -0,0 +1,6 @@ +package rediskey + +const ( + //用户api + MemberApiKey = "member_api:%s" +) diff --git a/common/statuscode/status_code.go b/common/statuscode/status_code.go index 12948cc..9e50797 100644 --- a/common/statuscode/status_code.go +++ b/common/statuscode/status_code.go @@ -2,6 +2,14 @@ package statuscode import "fmt" +type Response struct { + Status int `json:"status"` + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` + RequestID string `json:"RequestId"` +} + var StatusCodeZh = map[int]string{ Success: "成功", AccountExisted: "账号已存在", @@ -17,15 +25,22 @@ var StatusCodeZh = map[int]string{ MaxPriceExceeded: "超过最大接受单价", NoNumbers: "号码不足", RentalsNotFinished: "需要先完成部分租赁才能继续租赁", + Unauthorized: "未授权", + ApiUnActived: "API未激活", + InvalidParams: "参数错误", - SmsCancel: "短信验证码_手机号过期", - SmsNoActivation: "短信验证码_手机号不存在", - SmsWaitCode: "短信验证码_等待验证码", - SmsLongNumWaitCode: "短信验证码_长效号码已唤醒", - SmsNotExisted: "号码不存在", - SmsNotExpired: "号码未过期无法删除", - SmsNotAutoRenew: "短效号码无法自动续期", - SmsServiceUnavailable: "%s服务暂不可用", + SmsCancel: "短信验证码_手机号过期", + SmsNoActivation: "短信验证码_手机号不存在", + SmsWaitCode: "短信验证码_等待验证码", + SmsLongNumWaitCode: "短信验证码_长效号码已唤醒", + SmsNotExisted: "号码不存在", + SmsNotExpired: "号码未过期无法删除", + SmsNotAutoRenew: "短效号码无法自动续期", + SmsServiceUnavailable: "%s服务暂不可用", + SmsPlatformUnavailable: "通道不可用", + SmsInvalidType: "短信验证码_无效类型", + SmsOutOfStockOrUnavailable: "短信验证码_缺货或服务不可用", + SmsRentalRefundNotPermitted: "短信验证码_租赁退款不允许", } var StatusCodeEn = map[int]string{ @@ -43,15 +58,22 @@ var StatusCodeEn = map[int]string{ MaxPriceExceeded: "max price exceeded", NoNumbers: "no numbers", RentalsNotFinished: "need to finish some rentals before renting more", + Unauthorized: "un authorized", + ApiUnActived: "API un actived.", + InvalidParams: "invalid params", - SmsCancel: "sms code expired", - SmsNoActivation: "sms code not exist", - SmsWaitCode: "sms code wait for input", - SmsLongNumWaitCode: "sms code long num wake up", - SmsNotExisted: "number not exist", - SmsNotExpired: "number not expired, can not delete", - SmsNotAutoRenew: "num can not auto renew", - SmsServiceUnavailable: "%s service unavailable", + SmsCancel: "sms code expired", + SmsNoActivation: "sms code not exist", + SmsWaitCode: "sms code wait for input", + SmsLongNumWaitCode: "sms code long num wake up", + SmsNotExisted: "number not exist", + SmsNotExpired: "number not expired, can not delete", + SmsNotAutoRenew: "num can not auto renew", + SmsServiceUnavailable: "%s service unavailable", + SmsPlatformUnavailable: "channel unavailable", + SmsInvalidType: "sms type invalid", + SmsOutOfStockOrUnavailable: "sms out of stock or unavailable", + SmsRentalRefundNotPermitted: "sms rental refund not permitted", } // GetMsg 获取状态码对应的消息 @@ -108,6 +130,12 @@ const ( NoNumbers = 10012 // Need to finish some rentals before renting more RentalsNotFinished = 10013 + // 未授权 + Unauthorized = 10014 + // Api未激活 + ApiUnActived = 10015 + //参数错误 + InvalidParams = 10016 //短信验证码_手机号过期 SmsCancel = 20014 @@ -125,4 +153,12 @@ const ( SmsNotAutoRenew = 20020 //短信验证码_服务暂不可用 SmsServiceUnavailable = 20021 + //短信-通道不可用 + SmsPlatformUnavailable = 20022 + //短信-未知类型 + SmsInvalidType = 20023 + // 短信-缺货或服务不可用 + SmsOutOfStockOrUnavailable = 20024 + // 短信-租赁退款不允许 + SmsRentalRefundNotPermitted = 20025 ) diff --git a/config/extend.go b/config/extend.go index 6c90ebf..bd1a084 100644 --- a/config/extend.go +++ b/config/extend.go @@ -10,11 +10,19 @@ var ExtConfig Extend // // 使用方法: config.ExtConfig......即可!! type Extend struct { - AMap AMap // 这里配置对应配置文件的结构即可 - TrxGridUrl string - CliproxyUrl string //cliproxy服务地址 - CliproxyApiUrl string //cliproxy api地址 - DaisysmsUrl string //daisysms服务地址 + AMap AMap // 这里配置对应配置文件的结构即可 + TrxGridUrl string + TronApiKey string `yaml:"tronApiKey"` + CliproxyUrl string //cliproxy服务地址 + CliproxyApiUrl string //cliproxy api地址 + DaisysmsUrl string //daisysms服务地址 + SmsTextVerified SmsTextVerified `yaml:"smsTextVerified"` //短信验证服务配置 +} + +type SmsTextVerified struct { + Url string `yaml:"url"` + UserName string `yaml:"userName"` + ApiKey string `yaml:"apiKey"` } type AMap struct { diff --git a/config/settings.yml b/config/settings.yml index 7ffba14..0b710d6 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -51,12 +51,19 @@ settings: #trx api trxGridUrl: "https://api.trongrid.io" + tronApiKey: "223c129e-73f5-470f-9464-f9969846c134" #cliproxy url - cliproxyUrl: "https://f.cliproxy.com" + cliproxyUrl: "https://f.cliproxy.com" #cliproxy api url cliproxyApiUrl: "https://api.cliproxy.com" #daisysms api url daisysmsUrl: "https://daisysms.com/stubs/handler_api.php" + #sms text_verified + smsTextVerified: + url: "https://www.textverified.com" + userName: "webspan@proton.me" + apiKey: "ZQ0swXnsaPpeGdwa3c7gT9U9I1Oh9WoDHx0amuYovvaHuqd5u6B4NBBUSUBjR" + cache: redis: addr: 127.0.0.1:6379 diff --git a/go.mod b/go.mod index 329899e..e5017bb 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module go-admin -go 1.18 +go 1.24 require ( github.com/alibaba/sentinel-golang v1.0.4 @@ -14,8 +14,10 @@ require ( github.com/go-admin-team/go-admin-core v1.4.1-0.20220809101213-21187928f7d9 github.com/go-admin-team/go-admin-core/sdk v1.4.1-0.20220809101213-21187928f7d9 github.com/go-redis/redis/v8 v8.11.5 + github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.3.0 github.com/huaweicloud/huaweicloud-sdk-go-obs v3.21.12+incompatible + github.com/jinzhu/copier v0.4.0 github.com/mssola/user_agent v0.5.2 github.com/opentracing/opentracing-go v1.1.0 github.com/pkg/errors v0.9.1 @@ -77,7 +79,6 @@ require ( github.com/go-redis/redis/v7 v7.4.0 // indirect github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v4 v4.4.2 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 // indirect @@ -97,7 +98,6 @@ require ( github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect github.com/jackc/pgtype v1.11.0 // indirect github.com/jackc/pgx/v4 v4.16.1 // indirect - github.com/jinzhu/copier v0.4.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/utils/httphelper/http_helper.go b/utils/httphelper/http_helper.go index e235d4c..120a3dc 100644 --- a/utils/httphelper/http_helper.go +++ b/utils/httphelper/http_helper.go @@ -50,13 +50,13 @@ func (c *HTTPClient) applyHeaders(req *http.Request, customHeaders map[string]st // customHeaders: 自定义请求头,将覆盖默认请求头 // responseData: 用于存储响应数据的目标结构体(指针类型),如果为 nil 则表示不需要 JSON 解码 // rawResponse: 用于存储原始响应体字节切片(*[]byte),如果为 nil 则表示不需要原始响应 -func (c *HTTPClient) doRequest( +func (c *HTTPClient) DoRequest( method, path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}, rawResponse *[]byte, // 新增参数:指向字节切片的指针,用于存储原始响应 -) error { +) (int, error) { // 拼接完整的 URL url := c.BaseURL + path @@ -65,7 +65,7 @@ func (c *HTTPClient) doRequest( // 将请求体编码为 JSON jsonBody, err := json.Marshal(requestBody) if err != nil { - return fmt.Errorf("json marshal request body failed: %w", err) + return -1, fmt.Errorf("json marshal request body failed: %w", err) } reqBodyReader = bytes.NewBuffer(jsonBody) } @@ -73,7 +73,7 @@ func (c *HTTPClient) doRequest( // 创建新的 HTTP 请求 req, err := http.NewRequest(method, url, reqBodyReader) if err != nil { - return fmt.Errorf("create http request failed: %w", err) + return -1, fmt.Errorf("create http request failed: %w", err) } // 应用请求头 @@ -85,14 +85,14 @@ func (c *HTTPClient) doRequest( // 发送请求 resp, err := c.Client.Do(req) if err != nil { - return fmt.Errorf("send http request failed: %w", err) + return -1, fmt.Errorf("send http request failed: %w", err) } defer resp.Body.Close() // 检查 HTTP 状态码 if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest { bodyBytes, _ := io.ReadAll(resp.Body) // 读取错误响应体 - return fmt.Errorf("http request failed with status: %d, body: %s", resp.StatusCode, string(bodyBytes)) + return resp.StatusCode, fmt.Errorf("http request failed with status: %d, body: %s", resp.StatusCode, string(bodyBytes)) } // 解码响应(支持 gzip) @@ -100,7 +100,7 @@ func (c *HTTPClient) doRequest( if resp.Header.Get("Content-Encoding") == "gzip" { gzipReader, err := gzip.NewReader(resp.Body) if err != nil { - return fmt.Errorf("create gzip reader failed: %w", err) + return http.StatusOK, fmt.Errorf("create gzip reader failed: %w", err) } defer gzipReader.Close() reader = gzipReader @@ -109,7 +109,7 @@ func (c *HTTPClient) doRequest( // 首先读取整个响应体,然后决定如何处理 bodyBytes, err := io.ReadAll(reader) if err != nil { - return fmt.Errorf("read response body failed: %w", err) + return http.StatusOK, fmt.Errorf("read response body failed: %w", err) } // 如果提供了原始响应目标,则填充它 @@ -121,19 +121,19 @@ func (c *HTTPClient) doRequest( if responseData != nil { err = json.Unmarshal(bodyBytes, responseData) // 直接对字节切片使用 Unmarshal if err != nil { - return fmt.Errorf("json decode response body failed: %w", err) + return http.StatusOK, fmt.Errorf("json decode response body failed: %w", err) } } - return nil + return http.StatusOK, nil } // Get 发送 GET 请求 // path: 请求路径 // customHeaders: 自定义请求头 // responseData: 用于存储响应数据的目标结构体(指针类型) -func (c *HTTPClient) Get(path string, customHeaders map[string]string, responseData interface{}) error { - return c.doRequest(http.MethodGet, path, nil, customHeaders, responseData, nil) // rawResponse 传递 nil +func (c *HTTPClient) Get(path string, customHeaders map[string]string, responseData interface{}) (int, error) { + return c.DoRequest(http.MethodGet, path, nil, customHeaders, responseData, nil) // rawResponse 传递 nil } // Post 发送 POST 请求 @@ -141,8 +141,8 @@ func (c *HTTPClient) Get(path string, customHeaders map[string]string, responseD // requestBody: 请求体数据 // customHeaders: 自定义请求头 // responseData: 用于存储响应数据的目标结构体(指针类型) -func (c *HTTPClient) Post(path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}) error { - return c.doRequest(http.MethodPost, path, requestBody, customHeaders, responseData, nil) // rawResponse 传递 nil +func (c *HTTPClient) Post(path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}) (int, error) { + return c.DoRequest(http.MethodPost, path, requestBody, customHeaders, responseData, nil) // rawResponse 传递 nil } // PostWithContentType 发送 POST 请求,支持自定义 Content-Type(如 application/json 或 multipart/form-data) @@ -232,17 +232,19 @@ func (c *HTTPClient) PostWithContentType(path string, requestBody interface{}, c // requestBody: 请求体数据 // customHeaders: 自定义请求头 // responseData: 用于存储响应数据的目标结构体(指针类型) -func (c *HTTPClient) Put(path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}) error { - return c.doRequest(http.MethodPut, path, requestBody, customHeaders, responseData, nil) // rawResponse 传递 nil +// returns 状态码 和 错误信息 +func (c *HTTPClient) Put(path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}) (int, error) { + return c.DoRequest(http.MethodPut, path, requestBody, customHeaders, responseData, nil) // rawResponse 传递 nil } // Delete 发送 DELETE 请求 // path: 请求路径 // customHeaders: 自定义请求头 // responseData: 用于存储响应数据的目标结构体(指针类型) -func (c *HTTPClient) Delete(path string, customHeaders map[string]string, responseData interface{}) error { +// returns 状态码 和 错误信息 +func (c *HTTPClient) Delete(path string, customHeaders map[string]string, responseData interface{}) (int, error) { // DELETE 请求通常没有请求体,但某些 RESTful API 可能支持 - return c.doRequest(http.MethodDelete, path, nil, customHeaders, responseData, nil) // rawResponse 传递 nil + return c.DoRequest(http.MethodDelete, path, nil, customHeaders, responseData, nil) // rawResponse 传递 nil } // Patch 发送 PATCH 请求 @@ -250,20 +252,39 @@ func (c *HTTPClient) Delete(path string, customHeaders map[string]string, respon // requestBody: 请求体数据 // customHeaders: 自定义请求头 // responseData: 用于存储响应数据的目标结构体(指针类型) -func (c *HTTPClient) Patch(path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}) error { - return c.doRequest(http.MethodPatch, path, requestBody, customHeaders, responseData, nil) // rawResponse 传递 nil +// returns 状态码 和 错误信息 +func (c *HTTPClient) Patch(path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}) (int, error) { + return c.DoRequest(http.MethodPatch, path, requestBody, customHeaders, responseData, nil) // rawResponse 传递 nil } // GetRaw 发送 GET 请求并返回原始响应体 // path: 请求路径 // customHeaders: 自定义请求头 // 返回值: 原始响应体字节切片或错误 -func (c *HTTPClient) GetRaw(path string, customHeaders map[string]string) ([]byte, error) { +// 状态码 +// 错误信息 +func (c *HTTPClient) GetRaw(path string, customHeaders map[string]string) ([]byte, int, error) { var raw []byte // responseData 传递 nil,rawResponse 传递 &raw - err := c.doRequest(http.MethodGet, path, nil, customHeaders, nil, &raw) + statusCode, err := c.DoRequest(http.MethodGet, path, nil, customHeaders, nil, &raw) if err != nil { - return nil, err + return nil, statusCode, err } - return raw, nil + return raw, statusCode, nil +} + +// PostRaw 发送 POST 请求并返回原始响应体 +// path: 请求路径 +// customHeaders: 自定义请求头 +// 返回值: 原始响应体字节切片或错误 +// 状态码 +// 错误信息 +func (c *HTTPClient) PostRaw(path string, customHeaders map[string]string) ([]byte, int, error) { + var raw []byte + // responseData 传递 nil,rawResponse 传递 &raw + statusCode, err := c.DoRequest(http.MethodPost, path, nil, customHeaders, nil, &raw) + if err != nil { + return nil, statusCode, err + } + return raw, statusCode, nil }