Compare commits

...

11 Commits

Author SHA1 Message Date
837900462a 1、过期号码筛选 2025-10-21 14:29:27 +08:00
1a5cffa2cc 预扣款 2025-10-17 15:34:24 +08:00
73c6cf9844 1、暂存 2025-10-14 20:00:02 +08:00
996cd27fcb 1、续费和购买号码记录 2025-10-10 10:01:40 +08:00
9c826f4966 1、唤醒失败不影响获取 2025-10-08 03:51:06 +08:00
cbefd85f25 1、平台多ApiKey支持
2、平台号码跟系统号码对比自动续费
2025-09-11 20:01:00 +08:00
baa6552994 1增加接码过期时间 2025-09-09 20:45:37 +08:00
0f084e1461 1、查询服务余额重新计算 2025-09-02 17:03:37 +08:00
ab61808bf1 1、接码服务返回 续费金额 2025-09-02 14:35:53 +08:00
f73639a96c 1、增加接口 2025-08-28 14:11:09 +08:00
82d5acc873 1、新接textverified 2025-08-23 16:38:04 +08:00
72 changed files with 7616 additions and 643 deletions

View File

@ -0,0 +1,314 @@
package apis
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/api"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
"go-admin/common/middleware"
"go-admin/common/statuscode"
)
type SmsAbnormalNumber struct {
api.Api
}
// GetPage 获取异常号码统计列表
// @Summary 获取异常号码统计列表
// @Description 获取异常号码统计列表
// @Tags 异常号码统计
// @Param platformCode query string false "平台code"
// @Param phone query string false "电话号码"
// @Param pageSize query int false "页条数"
// @Param pageIndex query int false "页码"
// @Success 200 {object} response.Response{data=response.Page{list=[]models.SmsAbnormalNumber}} "{"code": 200, "data": [...]}"
// @Router /api/v1/sms-abnormal-number [get]
// @Security Bearer
func (e SmsAbnormalNumber) GetPage(c *gin.Context) {
req := dto.SmsAbnormalNumberGetPageReq{}
s := service.SmsAbnormalNumber{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
p := actions.GetPermissionFromContext(c)
list := make([]models.SmsAbnormalNumber, 0)
var count int64
err = s.GetPage(&req, p, &list, &count)
if err != nil {
e.Error(500, err, fmt.Sprintf("获取异常号码统计失败,\r\n失败信息 %s", err.Error()))
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// Get 获取异常号码统计
// @Summary 获取异常号码统计
// @Description 获取异常号码统计
// @Tags 异常号码统计
// @Param id path int false "id"
// @Success 200 {object} response.Response{data=models.SmsAbnormalNumber} "{"code": 200, "data": [...]}"
// @Router /api/v1/sms-abnormal-number/{id} [get]
// @Security Bearer
func (e SmsAbnormalNumber) Get(c *gin.Context) {
req := dto.SmsAbnormalNumberGetReq{}
s := service.SmsAbnormalNumber{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
var object models.SmsAbnormalNumber
p := actions.GetPermissionFromContext(c)
err = s.Get(&req, p, &object)
if err != nil {
e.Error(500, err, fmt.Sprintf("获取异常号码统计失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(object, "查询成功")
}
// Insert 创建异常号码统计
// @Summary 创建异常号码统计
// @Description 创建异常号码统计
// @Tags 异常号码统计
// @Accept application/json
// @Product application/json
// @Param data body dto.SmsAbnormalNumberInsertReq true "data"
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
// @Router /api/v1/sms-abnormal-number [post]
// @Security Bearer
func (e SmsAbnormalNumber) Insert(c *gin.Context) {
req := dto.SmsAbnormalNumberInsertReq{}
s := service.SmsAbnormalNumber{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
// 设置创建人
req.SetCreateBy(user.GetUserId(c))
err = s.Insert(&req)
if err != nil {
e.Error(500, err, fmt.Sprintf("创建异常号码统计失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(req.GetId(), "创建成功")
}
// Update 修改异常号码统计
// @Summary 修改异常号码统计
// @Description 修改异常号码统计
// @Tags 异常号码统计
// @Accept application/json
// @Product application/json
// @Param id path int true "id"
// @Param data body dto.SmsAbnormalNumberUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
// @Router /api/v1/sms-abnormal-number/{id} [put]
// @Security Bearer
func (e SmsAbnormalNumber) Update(c *gin.Context) {
req := dto.SmsAbnormalNumberUpdateReq{}
s := service.SmsAbnormalNumber{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c))
p := actions.GetPermissionFromContext(c)
err = s.Update(&req, p)
if err != nil {
e.Error(500, err, fmt.Sprintf("修改异常号码统计失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(req.GetId(), "修改成功")
}
// Delete 删除异常号码统计
// @Summary 删除异常号码统计
// @Description 删除异常号码统计
// @Tags 异常号码统计
// @Param data body dto.SmsAbnormalNumberDeleteReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
// @Router /api/v1/sms-abnormal-number [delete]
// @Security Bearer
func (e SmsAbnormalNumber) Delete(c *gin.Context) {
s := service.SmsAbnormalNumber{}
req := dto.SmsAbnormalNumberDeleteReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
// req.SetUpdateBy(user.GetUserId(c))
p := actions.GetPermissionFromContext(c)
err = s.Remove(&req, p)
if err != nil {
e.Error(500, err, fmt.Sprintf("删除异常号码统计失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(req.GetId(), "删除成功")
}
// 同步平台和系统的差异
func (e SmsAbnormalNumber) SyncState(c *gin.Context) {
s := service.SmsAbnormalNumber{}
err := e.MakeContext(c).
MakeOrm().
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
err = s.SyncState()
if err != nil {
e.Error(500, err, fmt.Sprintf("同步异常号码统计失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(nil, "同步成功")
}
// 重用号码
func (e SmsAbnormalNumber) ReUseAbnormalNumber(c *gin.Context) {
req := dto.ReUseAbnormalNumberReq{}
s := service.SmsAbnormalNumber{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
lang := "en"
userId, err := middleware.GetUserIdByApiKey(c)
if err != nil {
e.Logger.Error(err)
e.Error(statuscode.Unauthorized, nil, statuscode.GetMsg(statuscode.Unauthorized, lang))
return
}
resp, code := s.ReUseAbnormalNumber(userId, &req)
if code != statuscode.Success {
e.Error(code, nil, statuscode.GetMsg(code, lang))
return
}
e.OK(resp, "success")
}
// 清空数据
func (e SmsAbnormalNumber) Clean(c *gin.Context) {
s := service.SmsAbnormalNumber{}
err := e.MakeContext(c).
MakeOrm().
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
lang := "en"
if err != nil {
e.Logger.Error(err)
e.Error(statuscode.Unauthorized, nil, statuscode.GetMsg(statuscode.Unauthorized, lang))
return
}
err = s.Clean()
if err != nil {
e.Error(500, err, fmt.Sprintf("清除数据失败\r\n失败信息 %s", err.Error()))
return
}
e.OK(nil, "success")
}
// 批量取消
func (e SmsAbnormalNumber) BatchCancel(c *gin.Context) {
req := dto.BatchCancelAbnormalNumberReq{}
s := service.SmsAbnormalNumber{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
lang := "en"
if err != nil {
e.Logger.Error(err)
e.Error(statuscode.Unauthorized, nil, statuscode.GetMsg(statuscode.Unauthorized, lang))
return
}
err = s.BatchCancel(&req)
if err != nil {
e.Error(500, err, fmt.Sprintf("批量取消异常号码统计失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(nil, "success")
}

View File

@ -2,6 +2,7 @@ package apis
import (
"fmt"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/api"
@ -13,6 +14,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 +196,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 +222,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 +340,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 +462,321 @@ 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")
}
// 手动续费
func (e SmsPhone) ManualRenewal(c *gin.Context) {
req := dto.ManualRenewalReq{}
s := service.SmsRenewalLog{}
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
}
errorIds, err := s.ManualRenewal(req.ActivationIds)
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
var data string
if len(errorIds) > 0 {
data = strings.Join(errorIds, ",")
}
e.OK(data, "success")
}

View File

@ -0,0 +1,192 @@
package apis
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/api"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
)
type SmsPlatformKey struct {
api.Api
}
// GetPage 获取平台密钥管理列表
// @Summary 获取平台密钥管理列表
// @Description 获取平台密钥管理列表
// @Tags 平台密钥管理
// @Param platformCode query string false "平台code"
// @Param pageSize query int false "页条数"
// @Param pageIndex query int false "页码"
// @Success 200 {object} response.Response{data=response.Page{list=[]models.SmsPlatformKey}} "{"code": 200, "data": [...]}"
// @Router /api/v1/sms-platform-key [get]
// @Security Bearer
func (e SmsPlatformKey) GetPage(c *gin.Context) {
req := dto.SmsPlatformKeyGetPageReq{}
s := service.SmsPlatformKey{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
p := actions.GetPermissionFromContext(c)
list := make([]models.SmsPlatformKey, 0)
var count int64
err = s.GetPage(&req, p, &list, &count)
if err != nil {
e.Error(500, err, fmt.Sprintf("获取平台密钥管理失败,\r\n失败信息 %s", err.Error()))
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// Get 获取平台密钥管理
// @Summary 获取平台密钥管理
// @Description 获取平台密钥管理
// @Tags 平台密钥管理
// @Param id path int false "id"
// @Success 200 {object} response.Response{data=models.SmsPlatformKey} "{"code": 200, "data": [...]}"
// @Router /api/v1/sms-platform-key/{id} [get]
// @Security Bearer
func (e SmsPlatformKey) Get(c *gin.Context) {
req := dto.SmsPlatformKeyGetReq{}
s := service.SmsPlatformKey{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
var object models.SmsPlatformKey
p := actions.GetPermissionFromContext(c)
err = s.Get(&req, p, &object)
if err != nil {
e.Error(500, err, fmt.Sprintf("获取平台密钥管理失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(object, "查询成功")
}
// Insert 创建平台密钥管理
// @Summary 创建平台密钥管理
// @Description 创建平台密钥管理
// @Tags 平台密钥管理
// @Accept application/json
// @Product application/json
// @Param data body dto.SmsPlatformKeyInsertReq true "data"
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
// @Router /api/v1/sms-platform-key [post]
// @Security Bearer
func (e SmsPlatformKey) Insert(c *gin.Context) {
req := dto.SmsPlatformKeyInsertReq{}
s := service.SmsPlatformKey{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
// 设置创建人
req.SetCreateBy(user.GetUserId(c))
err = s.Insert(&req)
if err != nil {
e.Error(500, err, fmt.Sprintf("创建平台密钥管理失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(req.GetId(), "创建成功")
}
// Update 修改平台密钥管理
// @Summary 修改平台密钥管理
// @Description 修改平台密钥管理
// @Tags 平台密钥管理
// @Accept application/json
// @Product application/json
// @Param id path int true "id"
// @Param data body dto.SmsPlatformKeyUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
// @Router /api/v1/sms-platform-key/{id} [put]
// @Security Bearer
func (e SmsPlatformKey) Update(c *gin.Context) {
req := dto.SmsPlatformKeyUpdateReq{}
s := service.SmsPlatformKey{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c))
p := actions.GetPermissionFromContext(c)
err = s.Update(&req, p)
if err != nil {
e.Error(500, err, fmt.Sprintf("修改平台密钥管理失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(req.GetId(), "修改成功")
}
// Delete 删除平台密钥管理
// @Summary 删除平台密钥管理
// @Description 删除平台密钥管理
// @Tags 平台密钥管理
// @Param data body dto.SmsPlatformKeyDeleteReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
// @Router /api/v1/sms-platform-key [delete]
// @Security Bearer
func (e SmsPlatformKey) Delete(c *gin.Context) {
s := service.SmsPlatformKey{}
req := dto.SmsPlatformKeyDeleteReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
// req.SetUpdateBy(user.GetUserId(c))
p := actions.GetPermissionFromContext(c)
err = s.Remove(&req, p)
if err != nil {
e.Error(500, err, fmt.Sprintf("删除平台密钥管理失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(req.GetId(), "删除成功")
}

View File

@ -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))
}

View File

@ -12,6 +12,8 @@ import (
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
"go-admin/common/middleware"
"go-admin/common/statuscode"
)
type SmsRenewalLog struct {
@ -43,7 +45,7 @@ func (e SmsRenewalLog) GetPage(c *gin.Context) {
}
p := actions.GetPermissionFromContext(c)
list := make([]models.SmsRenewalLog, 0)
list := make([]dto.SmsRenewalLogResp, 0)
var count int64
err = s.GetPage(&req, p, &list, &count)
@ -85,7 +87,7 @@ func (e SmsRenewalLog) Get(c *gin.Context) {
return
}
e.OK( object, "查询成功")
e.OK(object, "查询成功")
}
// Insert 创建短信续期记录
@ -155,7 +157,7 @@ func (e SmsRenewalLog) Update(c *gin.Context) {
e.Error(500, err, fmt.Sprintf("修改短信续期记录失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK( req.GetId(), "修改成功")
e.OK(req.GetId(), "修改成功")
}
// Delete 删除短信续期记录
@ -188,5 +190,72 @@ func (e SmsRenewalLog) Delete(c *gin.Context) {
e.Error(500, err, fmt.Sprintf("删除短信续期记录失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK( req.GetId(), "删除成功")
e.OK(req.GetId(), "删除成功")
}
// 手动扣费
func (e SmsRenewalLog) ManualDeduct(c *gin.Context) {
req := dto.ManualDeductReq{}
s := service.SmsRenewalLog{}
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
}
userId, err := middleware.GetUserIdByApiKey(c)
if err != nil {
e.Logger.Error("获取用户id失败", err)
e.Error(statuscode.Unauthorized, nil, statuscode.GetMsg(statuscode.Unauthorized, "en"))
return
}
result, code := s.ManualDeduct(&req, userId)
if code != statuscode.Success {
e.Logger.Error(statuscode.GetMsg(code, "en"))
e.Error(code, nil, statuscode.GetMsg(code, "en"))
return
}
e.OK(result, "success")
}
// 获取详情根据tradeOrderNo
func (e *SmsRenewalLog) GetByTradeOrderNo(c *gin.Context) {
req := dto.ManualDeductDetailReq{}
s := service.SmsRenewalLog{}
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
}
userId, err := middleware.GetUserIdByApiKey(c)
if err != nil {
e.Logger.Error("获取用户id失败", err)
e.Error(statuscode.Unauthorized, nil, statuscode.GetMsg(statuscode.Unauthorized, "en"))
return
}
result, code := s.GetRenewalDetailByTradeOrderNo(&req, userId)
if code != statuscode.Success {
e.Logger.Error(statuscode.GetMsg(code, "en"))
e.Error(code, nil, statuscode.GetMsg(code, "en"))
return
}
e.OK(result, "success")
}

View File

@ -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, "获取翻译平台成功")
}

View File

@ -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, "")
}

View File

@ -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
}

View File

@ -0,0 +1,33 @@
package models
import (
"go-admin/common/models"
"time"
)
type SmsAbnormalNumber struct {
models.Model
Account string `json:"account" gorm:"type:varchar(255);comment:账号"`
PlatformCode string `json:"platformCode" gorm:"type:varchar(20);comment:平台code"`
ApiKey string `json:"apiKey" gorm:"type:varchar(255);comment:api key"`
Phone string `json:"phone" gorm:"type:varchar(30);comment:电话号码"`
ActivationId string `json:"activationId" gorm:"type:varchar(255);comment:激活id"`
ReceivedTime time.Time `json:"receivedTime" gorm:"type:datetime;comment:接收号码时间"`
Reused int `json:"reused" gorm:"type:tinyint(1);default:2;comment:是否重用 1-是 2-否"`
models.ModelTime
models.ControlBy
}
func (SmsAbnormalNumber) TableName() string {
return "sms_abnormal_number"
}
func (e *SmsAbnormalNumber) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SmsAbnormalNumber) GetId() interface{} {
return e.Id
}

View File

@ -10,21 +10,28 @@ 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"`
ApiKey string `json:"apiKey" gorm:"type:varchar(255);comment:apiKey"`
Service string `json:"service" gorm:"type:varchar(50);comment:sms 服务"`
ServiceCode string `json:"serviceCode" gorm:"type:varchar(30);comment:服务code"`
Type int `json:"type" gorm:"type:tinyint;comment:类型 0-短效 1-长效"`
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:过期时间"`
StartTime *time.Time `json:"startTime" gorm:"type:datetime;comment:开始时间"`
EndTime *time.Time `json:"endTime" gorm:"type:datetime;comment:结束时间 单次接码结束时间"`
Actived int `json:"actived" gorm:"type:tinyint;comment:是否激活(长期租赁如果第一次没接收到验证码 则不会激活号码) 1-未激活 2-已激活 3-已失效"`
Price decimal.Decimal `json:"price" gorm:"type:decimal(10,2);comment:价格"`
AutoRenewal int `json:"autoRenewal" gorm:"type:tinyint;comment:是否自动续费 1-自动续费 2-手动续费"`
Remark string `json:"remark" gorm:"type:varchar(255);comment:备注"`
BillingCycleId string `json:"billingCycleId" gorm:"type:varchar(50);comment:手动续费id"`
PlatformName string `json:"platformName" gorm:"-"`
models.ModelTime
models.ControlBy
}

View File

@ -0,0 +1,31 @@
package models
import (
"go-admin/common/models"
)
type SmsPlatformKey struct {
models.Model
PlatformCode string `json:"platformCode" gorm:"type:varchar(20);comment:平台code"`
Account string `json:"account" gorm:"type:varchar(50);comment:账号"`
ApiKey string `json:"apiKey" gorm:"type:varchar(500);comment:平台key"`
ApiSecret string `json:"apiSecret" gorm:"type:varchar(255);comment:平台私钥"`
Status int64 `json:"status" gorm:"type:tinyint;comment:状态 1-启用 2-禁用"`
Remark string `json:"remark" gorm:"type:varchar(255);comment:备注"`
models.ModelTime
models.ControlBy
}
func (SmsPlatformKey) TableName() string {
return "sms_platform_key"
}
func (e *SmsPlatformKey) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SmsPlatformKey) GetId() interface{} {
return e.Id
}

View File

@ -10,7 +10,7 @@ type SmsReceiveLog struct {
UserId int `json:"userId" gorm:"type:bigint;comment:用户id"`
Service string `json:"service" gorm:"type:varchar(30);comment:服务"`
ServiceCode string `json:"serviceCode" gorm:"type:varchar(30);comment:服务code"`
MessageId int `json:"messageId" gorm:"type:int;comment:短信id"`
MessageId string `json:"messageId" gorm:"type:int;comment:短信id"`
Phone string `json:"phone" gorm:"type:varchar(30);comment:号码"`
Code string `json:"code" gorm:"type:varchar(20);comment:验证码"`
Status int `json:"status" gorm:"type:tinyint;comment:状态 0-等待验证码 1-成功 2-失败"`

View File

@ -12,11 +12,23 @@ type SmsRenewalLog struct {
models.Model
PhoneId int `json:"phoneId" gorm:"type:bigint;comment:号码id"`
Phone string `json:"phone" gorm:"type:varchar(50);comment:号码"`
PayOrderNo string `json:"payOrderNo" gorm:"type:varchar(50);comment:本平台支付订单号"`
TradeOrderNo string `json:"tradeOrderNo" gorm:"type:varchar(50);comment:交易订单号"`
Status int `json:"status" gorm:"type:tinyint;comment:状态 1-处理中(冻结) 2-成功 3-失败(解冻)"`
Type int `json:"type" gorm:"type:tinyint;comment:类型 0-长效 1-短效"`
Category int `json:"category" gorm:"type:tinyint;comment:类别 1-购买号码 2-续费号码"`
UserId int `json:"userId" gorm:"type:bigint;comment:用户id"`
Amount decimal.Decimal `json:"amount" gorm:"type:decimal(10,2);comment:扣费金额"`
BeforeTime time.Time `json:"beforeTime" gorm:"type:datetime;comment:续费前过期时间"`
TargetTime *time.Time `json:"targetTime" gorm:"type:datetime;comment:续费目标过期时间"`
Period int `json:"period" gorm:"type:int;comment:时间段"`
Remark string `json:"remark" gorm:"type:varchar(255);comment:备注"`
Username string `json:"username" gorm:"->"`
PlatformCode string `json:"platformCode" gorm:"->"`
ApiKey string `json:"apiKey" gorm:"->"`
BillingCycleId string `json:"billingCycleId" gorm:"->"`
ActivationId string `json:"activationId" gorm:"->"`
models.ModelTime
models.ControlBy
}

View File

@ -2,15 +2,21 @@ package models
import (
"go-admin/common/models"
"github.com/shopspring/decimal"
)
type SmsServices struct {
models.Model
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
}

View File

@ -0,0 +1,37 @@
package router
import (
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
"go-admin/app/admin/apis"
"go-admin/common/actions"
"go-admin/common/middleware"
)
func init() {
routerCheckRole = append(routerCheckRole, registerSmsAbnormalNumberRouter)
}
// registerSmsAbnormalNumberRouter
func registerSmsAbnormalNumberRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := apis.SmsAbnormalNumber{}
r := v1.Group("/sms-abnormal-number").
Use(authMiddleware.MiddlewareFunc()).
Use(middleware.AuthCheckRole())
{
r.GET("", actions.PermissionAction(), api.GetPage)
r.GET("/:id", actions.PermissionAction(), api.Get)
r.POST("", api.Insert)
r.PUT("/:id", actions.PermissionAction(), api.Update)
r.DELETE("", api.Delete)
r.DELETE("clear", actions.PermissionAction(), api.Clean)
r.PUT("/batch-cancel", actions.PermissionAction(), api.BatchCancel)
}
r1 := v1.Group("/sms-abnormal-number")
{
r1.POST("/sync-state", api.SyncState) //同步差异
r1.PUT("/reuse", api.ReUseAbnormalNumber) //重用号码
}
}

View File

@ -29,12 +29,23 @@ 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)
r.GET("/:id", actions.PermissionAction(), api.Get)
r.POST("", api.Insert)
r.PUT("/:id", actions.PermissionAction(), api.Update)
r.PUT("/manual-renewal", api.ManualRenewal)
r.DELETE("", api.Delete)
}

View File

@ -0,0 +1,27 @@
package router
import (
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
"go-admin/app/admin/apis"
"go-admin/common/middleware"
"go-admin/common/actions"
)
func init() {
routerCheckRole = append(routerCheckRole, registerSmsPlatformKeyRouter)
}
// registerSmsPlatformKeyRouter
func registerSmsPlatformKeyRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := apis.SmsPlatformKey{}
r := v1.Group("/sms-platform-key").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
{
r.GET("", actions.PermissionAction(), api.GetPage)
r.GET("/:id", actions.PermissionAction(), api.Get)
r.POST("", api.Insert)
r.PUT("/:id", actions.PermissionAction(), api.Update)
r.DELETE("", api.Delete)
}
}

View File

@ -28,5 +28,6 @@ func registerSmsReceiveLogRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWT
r1 := v1.Group("sms-receive-log")
{
r1.POST("webhook", api.WebHook) //接收短信发送记录回调
r1.POST("text-verified-webhook", api.TextVerifiedWebHook) //接收TextVerified短信发送记录回调
}
}

View File

@ -5,8 +5,8 @@ import (
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
"go-admin/app/admin/apis"
"go-admin/common/middleware"
"go-admin/common/actions"
"go-admin/common/middleware"
)
func init() {
@ -16,6 +16,14 @@ func init() {
// registerSmsRenewalLogRouter
func registerSmsRenewalLogRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := apis.SmsRenewalLog{}
r1 := v1.Group("sms-renewal-log").Use(middleware.FrontedAuth)
{
// 手动扣费续期
r1.POST("manual-deduct", api.ManualDeduct)
//根据交易订单号获取续期详情
r1.GET("detail-byorderno", api.GetByTradeOrderNo)
}
r := v1.Group("/sms-renewal-log").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
{
r.GET("", actions.PermissionAction(), api.GetPage)

View File

@ -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) //获取服务价格
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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
}

View File

@ -0,0 +1,93 @@
package dto
import (
"go-admin/app/admin/models"
"go-admin/common/dto"
common "go-admin/common/models"
)
type SmsAbnormalNumberGetPageReq struct {
dto.Pagination `search:"-"`
PlatformCode string `form:"platformCode" search:"type:exact;column:platform_code;table:sms_abnormal_number" comment:"平台code"`
Phone string `form:"phone" search:"type:contains;column:phone;table:sms_abnormal_number" comment:"电话号码"`
SmsAbnormalNumberOrder
}
type SmsAbnormalNumberOrder struct {
Id string `form:"idOrder" search:"type:order;column:id;table:sms_abnormal_number"`
Account string `form:"accountOrder" search:"type:order;column:account;table:sms_abnormal_number"`
PlatformCode string `form:"platformCodeOrder" search:"type:order;column:platform_code;table:sms_abnormal_number"`
Phone string `form:"phoneOrder" search:"type:order;column:phone;table:sms_abnormal_number"`
CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:sms_abnormal_number"`
UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:sms_abnormal_number"`
DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:sms_abnormal_number"`
CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:sms_abnormal_number"`
UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:sms_abnormal_number"`
}
func (m *SmsAbnormalNumberGetPageReq) GetNeedSearch() interface{} {
return *m
}
type SmsAbnormalNumberInsertReq struct {
Id int `json:"-" comment:"主键id"` // 主键id
Account string `json:"account" comment:"账号"`
PlatformCode string `json:"platformCode" comment:"平台code"`
Phone string `json:"phone" comment:"电话号码"`
common.ControlBy
}
func (s *SmsAbnormalNumberInsertReq) Generate(model *models.SmsAbnormalNumber) {
if s.Id == 0 {
model.Model = common.Model{ Id: s.Id }
}
model.Account = s.Account
model.PlatformCode = s.PlatformCode
model.Phone = s.Phone
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
}
func (s *SmsAbnormalNumberInsertReq) GetId() interface{} {
return s.Id
}
type SmsAbnormalNumberUpdateReq struct {
Id int `uri:"id" comment:"主键id"` // 主键id
Account string `json:"account" comment:"账号"`
PlatformCode string `json:"platformCode" comment:"平台code"`
Phone string `json:"phone" comment:"电话号码"`
common.ControlBy
}
func (s *SmsAbnormalNumberUpdateReq) Generate(model *models.SmsAbnormalNumber) {
if s.Id == 0 {
model.Model = common.Model{ Id: s.Id }
}
model.Account = s.Account
model.PlatformCode = s.PlatformCode
model.Phone = s.Phone
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
}
func (s *SmsAbnormalNumberUpdateReq) GetId() interface{} {
return s.Id
}
// SmsAbnormalNumberGetReq 功能获取请求参数
type SmsAbnormalNumberGetReq struct {
Id int `uri:"id"`
}
func (s *SmsAbnormalNumberGetReq) GetId() interface{} {
return s.Id
}
// SmsAbnormalNumberDeleteReq 功能删除请求参数
type SmsAbnormalNumberDeleteReq struct {
Ids []int `json:"ids"`
}
func (s *SmsAbnormalNumberDeleteReq) GetId() interface{} {
return s.Ids
}

View File

@ -5,13 +5,19 @@ 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 服务"`
ServiceCode string `form:"serviceCode" search:"type:exact;column:service_code;table:sms_phone" comment:"服务code"`
PlatformCode string `form:"platformCode" search:"type:exact;column:platform_code;table:sms_phone" comment:"平台code"`
ServiceCode string `form:"serviceCode" search:"type:contains;column:service_code;table:sms_phone" comment:"服务code"`
Phone string `form:"phone" search:"type:contains;column:phone;table:sms_phone" comment:"号码"`
Type int `form:"type" search:"-" comment:"类型 0-短效 1-长效"`
IsActived int `form:"isActived" search:"-" comment:"是否可用"`
SmsPhoneOrder
}
@ -109,6 +115,7 @@ func (s *SmsPhoneDeleteReq) GetId() interface{} {
}
type GetNumberReq struct {
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:"时长(月)"`
@ -131,21 +138,28 @@ 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"`
StartTime *time.Time `json:"startTime" comment:"接码开始时间"`
EndTime *time.Time `json:"endTime" comment:"接码过期时间"`
}
func (s *WeakUpReq) Validate() error {
if s.ActivationId <= 0 {
if s.ActivationId == "" {
return errors.New("激活码id不能为空")
}
@ -166,13 +180,81 @@ func (s *DeleteMyNumberReq) Validate() error {
type SmsPhoneCancelNumberReq struct {
Id int `json:"id" comment:"短信号码id"`
ActivationId string `json:"activationId" comment:"短信id"`
}
type SmsPhoneChangeAutoRenewReq struct {
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"`
StartTime *time.Time `json:"-" comment:"唤醒后单次接码开始时间"`
EndTime *time.Time `json:"-" comment:"唤醒后单次接码过期时间"`
MessageId string `json:"-" comment:"消息id"`
Id string `json:"id"`
BillingCycleId string `json:"billingCycleId" comment:"续租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"`
StartTime *time.Time `json:"startTime"`
EndTime *time.Time `json:"endTime"`
}
type OpenWakeUpReq struct {
PlatformCode string `json:"platformCode" comment:"短信平台"`
ActivationId string `json:"activationId" comment:"平台id"`
}
type ReUseAbnormalNumberReq struct {
BeginTime int64 `json:"beginTime" comment:"开始时间"`
PlatformCode string `json:"platformCode" comment:"短信平台"`
ServiceCode string `json:"serviceCode" comment:"服务code"`
}
// 批量取消
type BatchCancelAbnormalNumberReq struct {
Ids []int `json:"ids" comment:"短信号码id"`
}
type ManualRenewalReq struct {
ActivationIds []string `json:"activationIds" comment:"激活码id"`
TradeOrderNo string `json:"tradeOrderNo" comment:"交易订单号"`
}
// 手动续期回调请求
type ManualRenewCallBackReq struct {
TradeOrderNo string `json:"tradeOrderNo" comment:"交易订单号"`
PayOrderNo string `json:"payOrderNo" comment:"本平台支付订单号"`
Status int `json:"status" comment:"状态 1-处理中 2-失败 3-回滚 4-成功"`
Remark string `json:"remark" comment:"备注"`
}
// 获取号码响应
type SmsPhoneGetNumberResp struct {
ActivationId string `json:"activationId" comment:"激活码id"`
Phone string `json:"phone" comment:"号码"`
ExpireTime *time.Time `json:"expireTime" comment:"过期时间"`
StartTime *time.Time `json:"startTime" comment:"唤醒后单次接码开始时间"`
EndTime *time.Time `json:"endTime" comment:"唤醒后单次接码过期时间"`
BillingCycleId string `json:"billingCycleId" comment:"续租id"`
}

View File

@ -0,0 +1,117 @@
package dto
import (
"go-admin/app/admin/models"
"go-admin/common/dto"
common "go-admin/common/models"
)
type SmsPlatformKeyGetPageReq struct {
dto.Pagination `search:"-"`
PlatformCode string `form:"platformCode" search:"type:exact;column:platform_code;table:sms_platform_key" comment:"平台code"`
SmsPlatformKeyOrder
}
type SmsPlatformKeyOrder struct {
Id string `form:"idOrder" search:"type:order;column:id;table:sms_platform_key"`
PlatformCode string `form:"platformCodeOrder" search:"type:order;column:platform_code;table:sms_platform_key"`
ApiKey string `form:"apiKeyOrder" search:"type:order;column:api_key;table:sms_platform_key"`
ApiSecret string `form:"apiSecretOrder" search:"type:order;column:api_secret;table:sms_platform_key"`
Status string `form:"statusOrder" search:"type:order;column:status;table:sms_platform_key"`
Remark string `form:"remarkOrder" search:"type:order;column:remark;table:sms_platform_key"`
CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:sms_platform_key"`
UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:sms_platform_key"`
DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:sms_platform_key"`
CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:sms_platform_key"`
UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:sms_platform_key"`
}
func (m *SmsPlatformKeyGetPageReq) GetNeedSearch() interface{} {
return *m
}
type SmsPlatformKeyInsertReq struct {
Id int `json:"-" comment:"主键id"` // 主键id
PlatformCode string `json:"platformCode" comment:"平台code"`
Account string `json:"account" comment:"account"`
ApiKey string `json:"apiKey" comment:"平台key"`
ApiSecret string `json:"apiSecret" comment:"平台私钥"`
Status int64 `json:"status" comment:"状态 1-启用 2-禁用"`
Remark string `json:"remark" comment:"备注"`
common.ControlBy
}
func (s *SmsPlatformKeyInsertReq) Generate(model *models.SmsPlatformKey) {
if s.Id == 0 {
model.Model = common.Model{Id: s.Id}
}
model.PlatformCode = s.PlatformCode
model.Account = s.Account
model.ApiKey = s.ApiKey
model.ApiSecret = s.ApiSecret
model.Status = s.Status
model.Remark = s.Remark
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
}
func (s *SmsPlatformKeyInsertReq) GetId() interface{} {
return s.Id
}
type SmsPlatformKeyUpdateReq struct {
Id int `uri:"id" comment:"主键id"` // 主键id
PlatformCode string `json:"platformCode" comment:"平台code"`
Account string `json:"account" comment:"平台账号"`
ApiKey string `json:"apiKey" comment:"平台key"`
ApiSecret string `json:"apiSecret" comment:"平台私钥"`
Status int64 `json:"status" comment:"状态 1-启用 2-禁用"`
Remark string `json:"remark" comment:"备注"`
common.ControlBy
}
func (s *SmsPlatformKeyUpdateReq) Generate(model *models.SmsPlatformKey) {
if s.Id == 0 {
model.Model = common.Model{Id: s.Id}
}
model.PlatformCode = s.PlatformCode
model.Account = s.Account
model.ApiKey = s.ApiKey
model.ApiSecret = s.ApiSecret
model.Status = s.Status
model.Remark = s.Remark
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
}
func (s *SmsPlatformKeyUpdateReq) GetId() interface{} {
return s.Id
}
// SmsPlatformKeyGetReq 功能获取请求参数
type SmsPlatformKeyGetReq struct {
Id int `uri:"id"`
}
func (s *SmsPlatformKeyGetReq) GetId() interface{} {
return s.Id
}
// SmsPlatformKeyDeleteReq 功能删除请求参数
type SmsPlatformKeyDeleteReq struct {
Ids []int `json:"ids"`
}
func (s *SmsPlatformKeyDeleteReq) GetId() interface{} {
return s.Ids
}
type SmsPlatformKeyQueueDto struct {
PlatformCode string `json:"platformCode" comment:"平台code"`
Account string `json:"account" comment:"平台账号"`
ApiKey string `json:"apiKey" comment:"平台key"`
ApiSecret string `json:"apiSecret" comment:"平台私钥"`
}
type SmsPlatformKeyGroupDto struct {
PlatformCode string `json:"platformCode"`
Count int `json:"count"`
}

View File

@ -13,6 +13,8 @@ import (
type SmsRenewalLogGetPageReq struct {
dto.Pagination `search:"-"`
Type int64 `form:"type" search:"type:exact;column:type;table:sms_renewal_log" comment:"类型 0-长效 1-短效"`
Category int `form:"category" search:"type:exact;column:category;table:sms_renewal_log" comment:"类别 0-普通续费 1-号码补偿"`
UserName string `form:"userName" search:"-"`
SmsRenewalLogOrder
}
@ -108,3 +110,43 @@ type SmsRenewalLogDeleteReq struct {
func (s *SmsRenewalLogDeleteReq) GetId() interface{} {
return s.Ids
}
type SmsRenewalLogResp struct {
Id int `json:"id" comment:"主键id"` // 主键id
PhoneId int `json:"phoneId" comment:"号码id"`
Phone string `json:"phone" comment:"电话号码"`
TradeOrderNo string `json:"tradeOrderNo" comment:"交易订单号"`
PayOrderNo string `json:"payOrderNo" comment:"支付订单号"`
Type int `json:"type" comment:"类型 0-长效 1-短效"`
Category int `json:"category" comment:"类别 1-购买 2-续期"`
UserId int `json:"userId" comment:"用户id"`
Amount decimal.Decimal `json:"amount" comment:"扣费金额"`
BeforeTime time.Time `json:"beforeTime" comment:"续费前过期时间"`
Period int `json:"period" comment:"时间(天)"`
CreatedAt time.Time `json:"createdAt" comment:"创建时间"`
Status int `json:"status" comment:"状态 1-预扣费 2-成功 3-失败"`
// UpdatedAt time.Time `json:"updatedAt" comment:"更新时间"`
// DeletedAt time.Time `json:"deletedAt" comment:"删除时间"`
UserName string `json:"username" comment:"用户名"`
}
// 手动扣费
type ManualDeductReq struct {
ActivationId string `json:"activationId" comment:"激活id"`
TradeOrderNo string `json:"tradeOrderNo" comment:"交易订单号"`
BeginTime time.Time `json:"beginTime" comment:"开始时间"`
}
type ManualDeductResp struct {
ActivationId string `json:"activationId" comment:"激活id"`
TradeOrderNo string `json:"tradeOrderNo" comment:"交易订单号"`
PayOrderNo string `json:"payOrderNo" comment:"支付订单号"`
BeginTime time.Time `json:"beginTime" comment:"开始时间"`
EndTime *time.Time `json:"endTime" comment:"结束时间"`
Status int `json:"status" comment:"状态 1-预扣费 2-成功 3-失败"`
}
// 续费详情
type ManualDeductDetailReq struct {
TradeOrderNo string `json:"tradeOrderNo" comment:"交易订单号"`
}

View File

@ -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
}
@ -30,6 +36,11 @@ type SmsServicesInsertReq struct {
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 // 添加这而,需要记录是被谁创建的
}
@ -50,6 +66,11 @@ type SmsServicesUpdateReq struct {
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,39 @@ func (s *SmsServicesDeleteReq) GetId() interface{} {
}
type SmsServicesGetListResp struct {
Name string `json:"name" 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:"长号码价格"`
RenewLongPrice decimal.Decimal `json:"renewLongPrice" comment:"续费长号码价格"`
}
type SmsGetPriceReq struct {
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"`
}

View File

@ -0,0 +1,259 @@
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"`
ApiKey string `json:"apiKey"`
// 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"`
IsIncludedForNextRenewal bool `json:"isIncludedForNextRenewal" comment:"是否自动续期"`
}
// 长效详情
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"`
BillingCycleId string `json:"billingCycleId" comment:"续租id"`
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"`
}
type TextVerifiedGetNumbersResp struct {
HasNext bool `json:"hasNext" comment:"是否有下一页"`
Links TextVerifiedGetNumbersLinksResp `json:"links"`
Data []VerificationDTO `json:"data"`
}
type TextVerifiedGetRentalListResp struct {
HasNext bool `json:"hasNext" comment:"是否有下一页"`
Links TextVerifiedGetNumbersLinksResp `json:"links"`
Data []VerificationRentalDetailResp `json:"data"`
}
type TextVerifiedGetNumbersLinksResp struct {
Next *TextVerifiedResp `json:"next"`
}
type TextVerifiedManualRenewalResp struct {
CreatedAt time.Time `json:"createdAt" comment:"创建时间"`
Id string `json:"id" comment:"id"`
ExcludeRentals []TextVerifiedManualRenewalRental `json:"excludeRentals" comment:"排除的长效详情"`
IncludeRentals []TextVerifiedManualRenewalRental `json:"includeRentals" comment:"包含的长效详情"`
IsPaidFor bool `json:"isPaidFor" comment:"是否已支付"`
TotalCost decimal.Decimal `json:"totalCost" comment:"总费用"`
}
type TextVerifiedManualRenewalRental struct {
Number string `json:"number" comment:"号码"`
Rental TextVerifiedResp `json:"rental" comment:"长效详情"`
RenewalCost decimal.Decimal `json:"renewalCost" comment:"续费费用"`
ServiceName string `json:"serviceName" comment:"服务名称"`
AlreadyRenewed bool `json:"alreadyRenewed" comment:"是否已续费"`
}
// 续费周期详情
type TextVerifiedGetBillingCycleResp struct {
Id string `json:"id" comment:"id"`
RenewedThrough time.Time `json:"renewedThrough" comment:"已续费到"`
BillingCycleEndsAt time.Time `json:"billingCycleEndsAt" comment:"计费周期结束时间"`
NextAutoRenewAttempt time.Time `json:"nextAutoRenewAttempt" comment:"下次自动续费时间"`
State string `json:"state" comment:"是否激活 active"`
}

View File

@ -0,0 +1,122 @@
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
}
apiKey := fmt.Sprintf(rediskey.MemberApiKey, apiEntity.ApiKey)
val, err := sonic.MarshalString(apiEntity)
if err != nil {
e.Log.Errorf("用户api 初始化失败")
return err
}
if err := redishelper.DefaultRedis.SetString(apiKey, val); err != nil {
e.Log.Errorf("用户api 初始化失败")
return err
}
return nil
}

View File

@ -115,7 +115,7 @@ func (e MemberRecharge) CustomRecharge(req *dto.MemberRechargeCustomRechargeReq,
return errors.New("server error")
}
cacheExpireDuration := time.Duration(orderExpireTime+1) * time.Minute
cacheExpireDuration := time.Duration(orderExpireTime+10) * time.Minute
key := fmt.Sprintf("pre_order:%s", amount)
err = e.Orm.Transaction(func(tx *gorm.DB) error {
if err1 := tx.Create(&data).Error; err1 != nil {

View File

@ -0,0 +1 @@
package service

View File

@ -0,0 +1,434 @@
package service
import (
"errors"
"time"
"github.com/go-admin-team/go-admin-core/sdk/service"
"github.com/shopspring/decimal"
"gorm.io/gorm"
"go-admin/app/admin/models"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
cDto "go-admin/common/dto"
"go-admin/common/global"
"go-admin/common/statuscode"
"go-admin/utils/utility"
)
type SmsAbnormalNumber struct {
service.Service
}
func (e *SmsAbnormalNumber) Clean() error {
if err := e.Orm.Exec("truncate table sms_abnormal_number").Error; err != nil {
return err
}
return nil
}
// 批量取消
func (e *SmsAbnormalNumber) BatchCancel(req *dto.BatchCancelAbnormalNumberReq) error {
var datas []models.SmsAbnormalNumber
if err := e.Orm.Model(&models.SmsAbnormalNumber{}).Where("id in ?", req.Ids).Find(&datas).Error; err != nil {
return err
}
utility.SafeGo(func() {
CancelFunc(e, datas)
})
return nil
}
func CancelFunc(e *SmsAbnormalNumber, datas []models.SmsAbnormalNumber) {
phoneService := SmsPhone{Service: e.Service}
for _, item := range datas {
phoneService.ChangeAutoRenewManage(item.PlatformCode, item.ApiKey, item.ActivationId, false)
time.Sleep(200 * time.Millisecond)
}
}
// 重用号码
func (e *SmsAbnormalNumber) ReUseAbnormalNumber(userId int, req *dto.ReUseAbnormalNumberReq) (dto.OpenGetNumberResp, int) {
resp := dto.OpenGetNumberResp{}
balanceService := MemberBalance{Service: e.Service}
config := dto.GetSysConfigByKEYForServiceResp{}
configReq := dto.SysConfigByKeyReq{}
configService := SysConfig{Service: e.Service}
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 resp, statuscode.ServerError
}
if serviceItem.Code == "" {
e.Log.Error("短信服务不存在")
return resp, statuscode.ServerError
}
switch {
case req.PlatformCode == global.SmsPlatformDaisysms:
price = serviceItem.LongPrice
configReq.ConfigKey = "long_number_premium_daisysms"
case req.PlatformCode == global.SmsPlatformTextVerified:
price = serviceItem.LongPrice
configReq.ConfigKey = "long_number_premium_textverified"
}
err = configService.GetWithKey(&configReq, &config)
if err != nil {
return resp, statuscode.ServerError
}
if config.ConfigValue == "" {
e.Log.Errorf("短期长期租赁浮动百分比不能为空")
return resp, statuscode.ServerError
}
percent, err := decimal.NewFromString(config.ConfigValue)
if err != nil {
e.Log.Errorf("短期或长期租赁费用格式错误")
return resp, 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 resp, statuscode.ServerError
}
balance := balanceService.GetBalance(userId)
if balance.LessThan(price) {
e.Log.Error("余额不足")
return resp, statuscode.BalanceNotEnough
}
abnomarlNumber, err := e.GetReused(req.PlatformCode, req.BeginTime)
if err != nil {
return resp, statuscode.ServerError
}
expireTime := abnomarlNumber.ReceivedTime.AddDate(0, 0, 30)
startTime := time.Now()
endTime := startTime
smsPhone := models.SmsPhone{}
smsPhone.PlatformCode = req.PlatformCode
smsPhone.Phone = abnomarlNumber.Phone
smsPhone.UserId = userId
smsPhone.Service = serviceItem.Name
smsPhone.ServiceCode = req.ServiceCode
smsPhone.Type = 1
smsPhone.Period = 1
smsPhone.ActivationId = abnomarlNumber.ActivationId
smsPhone.MessageId = abnomarlNumber.ActivationId
smsPhone.NewActivationId = abnomarlNumber.ActivationId
smsPhone.Actived = 1
smsPhone.StartTime = &startTime
smsPhone.EndTime = &endTime
// smsPhone.ApiKey = apiInfo.ApiKey
smsPhone.Price = price
smsPhone.ExpireTime = &expireTime
//平台不允许取消
if req.PlatformCode == global.SmsPlatformTextVerified {
smsPhone.Actived = 2
}
smsPhone.Status = 1
err = e.Orm.Transaction(func(tx *gorm.DB) error {
if err1 := tx.Save(&smsPhone).Error; err1 != nil {
e.Log.Errorf("获取手机号失败", err1)
return err1
}
if err1 := tx.Exec("UPDATE member_balance SET balance = balance -? WHERE user_id =?", price, userId).Error; err1 != nil {
e.Log.Errorf("更新余额失败", err1)
return err1
}
return nil
})
if err != nil {
return resp, statuscode.ServerError
}
resp.ActivationId = smsPhone.NewActivationId
resp.ExpireTime = smsPhone.ExpireTime
resp.Number = smsPhone.Phone
resp.StartTime = smsPhone.StartTime
resp.EndTime = smsPhone.EndTime
return resp, statuscode.Success
}
// 获取重用
func (e *SmsAbnormalNumber) GetReused(platformCode string, beginTime int64) (models.SmsAbnormalNumber, error) {
var smsAbnormalNumber models.SmsAbnormalNumber
query := e.Orm.Model(&smsAbnormalNumber).
Where("platform_code = ? AND reused = ?", platformCode, 2)
if beginTime > 0 {
query.Where("received_time >= ?", time.Unix(beginTime, 0))
}
err := query.Order("received_time asc").First(&smsAbnormalNumber).Error
if err != nil {
e.Log.Errorf("SmsAbnormalNumberService GetReused error:%s \r\n", err)
return smsAbnormalNumber, err
}
if err := e.Orm.Model(&models.SmsAbnormalNumber{}).
Where("id = ?", smsAbnormalNumber.Id).
Update("reused", 1).Error; err != nil {
e.Log.Errorf("SmsAbnormalNumberService GetReused error:%s \r\n", err)
return smsAbnormalNumber, err
}
return smsAbnormalNumber, nil
}
// GetPage 获取SmsAbnormalNumber列表
func (e *SmsAbnormalNumber) GetPage(c *dto.SmsAbnormalNumberGetPageReq, p *actions.DataPermission, list *[]models.SmsAbnormalNumber, count *int64) error {
var err error
var data models.SmsAbnormalNumber
err = e.Orm.Model(&data).
Scopes(
cDto.MakeCondition(c.GetNeedSearch()),
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
actions.Permission(data.TableName(), p),
).
Find(list).Limit(-1).Offset(-1).
Count(count).Error
if err != nil {
e.Log.Errorf("SmsAbnormalNumberService GetPage error:%s \r\n", err)
return err
}
return nil
}
// Get 获取SmsAbnormalNumber对象
func (e *SmsAbnormalNumber) Get(d *dto.SmsAbnormalNumberGetReq, p *actions.DataPermission, model *models.SmsAbnormalNumber) error {
var data models.SmsAbnormalNumber
err := e.Orm.Model(&data).
Scopes(
actions.Permission(data.TableName(), p),
).
First(model, d.GetId()).Error
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
err = errors.New("查看对象不存在或无权查看")
e.Log.Errorf("Service GetSmsAbnormalNumber error:%s \r\n", err)
return err
}
if err != nil {
e.Log.Errorf("db error:%s", err)
return err
}
return nil
}
// Insert 创建SmsAbnormalNumber对象
func (e *SmsAbnormalNumber) Insert(c *dto.SmsAbnormalNumberInsertReq) error {
var err error
var data models.SmsAbnormalNumber
c.Generate(&data)
err = e.Orm.Create(&data).Error
if err != nil {
e.Log.Errorf("SmsAbnormalNumberService Insert error:%s \r\n", err)
return err
}
return nil
}
// Update 修改SmsAbnormalNumber对象
func (e *SmsAbnormalNumber) Update(c *dto.SmsAbnormalNumberUpdateReq, p *actions.DataPermission) error {
var err error
var data = models.SmsAbnormalNumber{}
e.Orm.Scopes(
actions.Permission(data.TableName(), p),
).First(&data, c.GetId())
c.Generate(&data)
db := e.Orm.Save(&data)
if err = db.Error; err != nil {
e.Log.Errorf("SmsAbnormalNumberService Save error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权更新该数据")
}
return nil
}
// Remove 删除SmsAbnormalNumber
func (e *SmsAbnormalNumber) Remove(d *dto.SmsAbnormalNumberDeleteReq, p *actions.DataPermission) error {
var data models.SmsAbnormalNumber
db := e.Orm.Model(&data).
Scopes(
actions.Permission(data.TableName(), p),
).Delete(&data, d.GetId())
if err := db.Error; err != nil {
e.Log.Errorf("Service RemoveSmsAbnormalNumber error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权删除该数据")
}
return nil
}
// 获取平台上不再系统内的自动续期号码
func (e *SmsAbnormalNumber) SyncState() error {
textVerified := SmsTextVerified{Service: e.Service}
// daiSysms := SmsDaisysms{Service: e.Service} 没有列表api
textVerifiedNumbers, err := textVerified.GetAllPlatformNumbers()
if err != nil {
e.Log.Errorf("获取长效号码列表失败 %v", err)
} else {
// 获取系统内textverified平台的自动续期号码
// 参数: 平台代码, 号码类型(1=长效), 自动续费(1=开启), 激活状态(2=已激活)
systemNumbers, err := e.GetPhoneByPlatformCode(global.SmsPlatformTextVerified, 1, 1, 2)
if err != nil {
e.Log.Errorf("获取系统内textverified自动续期号码失败: %v", err)
return err
}
e.Log.Infof("系统内textverified自动续期号码数量: %d", len(systemNumbers))
// 比较平台号码和系统号码,找出差异
missingNumbers := e.findMissingNumbers(textVerifiedNumbers, systemNumbers)
if len(missingNumbers) > 0 {
e.Log.Warnf("发现%d个平台开启自动续费但系统内缺失的号码", len(missingNumbers))
err = e.Orm.Transaction(func(tx *gorm.DB) error {
if err1 := tx.Model(&models.SmsAbnormalNumber{}).
Unscoped().
Where(" platform_code =?", global.SmsPlatformTextVerified).
Delete(&models.SmsAbnormalNumber{}).
Error; err1 != nil {
return err1
}
if err1 := tx.CreateInBatches(missingNumbers, 100).Error; err1 != nil {
return err1
}
return nil
})
if err != nil {
e.Log.Errorf("同步后保存差异数据失败 %v", err)
}
} else {
e.Log.Infof("所有平台自动续期号码在系统内都存在")
}
}
return nil
}
// findMissingNumbers 比较平台号码和系统号码,找出平台上有但系统内没有的号码
// 参数:
// - platformNumbers: 平台上的号码列表
// - systemNumbers: 系统内的号码列表
//
// 返回:
// - []dto.VerificationDTO: 平台上有但系统内缺失的号码列表
func (e *SmsAbnormalNumber) findMissingNumbers(platformNumbers map[string][]dto.VerificationDTO, systemNumbers []models.SmsPhone) []models.SmsAbnormalNumber {
// 创建系统号码的映射表,用于快速查找
systemNumberMap := make(map[string]bool)
for _, sysPhone := range systemNumbers {
// 使用号码作为key
systemNumberMap[sysPhone.Phone] = true
}
// 找出平台上有但系统内没有的号码
missingNumbers := make([]models.SmsAbnormalNumber, 0)
for account, platformPhone := range platformNumbers {
for _, v := range platformPhone {
if v.State == "renewableActive" && v.IsIncludedForNextRenewal && !systemNumberMap[v.Number] {
missingNumbers = append(missingNumbers, models.SmsAbnormalNumber{
PlatformCode: global.SmsPlatformTextVerified,
Account: account,
Phone: v.Number,
ActivationId: v.ID,
ReceivedTime: v.CreatedAt,
ApiKey: v.ApiKey,
})
}
}
}
return missingNumbers
}
// GetPhoneByPlatformCode 循环获取对应平台自动续期的号码
// 为避免一次性查询过多数据使用分页查询每次最多查询100条记录
// @param platformCode 平台代码 (如: textverified, daisysms)
// @return []models.SmsPhone 号码列表
// @return error 错误信息
func (e *SmsAbnormalNumber) GetPhoneByPlatformCode(platformCode string, numberType, autoRenewal, status int) ([]models.SmsPhone, error) {
var phones []models.SmsPhone
var allPhones []models.SmsPhone
// 分页参数
pageSize := 1000 // 每页最多1000条记录
offset := 0
// 循环分页查询,避免一次性查询过多数据
for {
// 查询条件:
// 1. platform_code = platformCode (指定平台)
// 2. type = numberType (号码类型)
// 3. auto_renewal = autoRenewal (自动续费状态)
// 4. actived = status (激活状态)
err := e.Orm.Model(&models.SmsPhone{}).
Where("platform_code = ? AND type = ? AND auto_renewal = ? AND actived = ? AND expire_time > ?",
platformCode, numberType, autoRenewal, status, time.Now()).
Order("id ASC").
Limit(pageSize).
Offset(offset).
Find(&phones).Error
if err != nil {
e.Log.Errorf("查询平台[%s]自动续期号码失败: %v", platformCode, err)
return nil, err
}
// 如果没有查询到数据,说明已经查询完毕
if len(phones) == 0 {
break
}
// 将本次查询结果添加到总结果中
allPhones = append(allPhones, phones...)
// 如果本次查询结果少于pageSize说明已经是最后一页
if len(phones) < pageSize {
break
}
// 更新偏移量,准备查询下一页
offset += pageSize
}
e.Log.Infof("平台[%s]查询到%d个自动续期号码", platformCode, len(allPhones))
return allPhones, nil
}

View File

@ -0,0 +1,20 @@
package service
import (
"testing"
"github.com/go-admin-team/go-admin-core/logger"
)
// 同步差异号码
func TestSyncSat(t *testing.T) {
db := initSetting()
smsAbnormalNumber := SmsAbnormalNumber{}
smsAbnormalNumber.Orm = db
smsAbnormalNumber.Log = logger.NewHelper(logger.DefaultLogger)
if err := smsAbnormalNumber.SyncState(); err != nil {
t.Errorf("同步差异号码失败 %v", err)
}
}

View File

@ -0,0 +1,301 @@
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() {
smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log)
apiInfo, err := smsPlatformKeyRedis.GetRoundRobinKey(global.SmsPlatformDaisysms)
prices, err := e.GetPrices(apiInfo)
if err != nil {
e.Log.Errorf("GetPrices error: %v", err)
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(apiInfo *dto.SmsPlatformKeyQueueDto, getType int, serviceCode string, maxPrice decimal.Decimal, period int) (int, string, int) {
acitvationId := 0
result := ""
resultCode := statuscode.Success
url := fmt.Sprintf("?api_key=%s&action=getNumber&service=%s", apiInfo.ApiKey, 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, apiInfo *dto.SmsPlatformKeyQueueDto) int {
url := fmt.Sprintf("?api_key=%s&action=setStatus&id=%d&status=6", apiInfo.ApiKey, id)
client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil)
bytes, _, err := client.GetRaw(url, nil)
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
}
}
// GetCodeForApi 获取验证码
// messageId 短信id
// return 验证码, 状态码
func (e *SmsDaisysms) GetCodeForApi(messageId string, apiInfo *dto.SmsPlatformKeyQueueDto) (string, int) {
result := ""
code := statuscode.Success
url := fmt.Sprintf("?api_key=%s&action=getStatus&id=%s", apiInfo.ApiKey, messageId)
client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil)
bytes, _, err := client.GetRaw(url, nil)
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, apiInfo *dto.SmsPlatformKeyQueueDto) (int, int) {
result := 0
url := fmt.Sprintf("?api_key=%s&action=getExtraActivation&activationId=%s", apiInfo.ApiKey, activationId)
client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil)
bytes, _, err1 := client.GetRaw(url, nil)
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, apiInfo *dto.SmsPlatformKeyQueueDto) int {
url := fmt.Sprintf("?api_key=%s&action=keep&id=%s", apiInfo.ApiKey, activationId)
client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil)
bytes, _, err := client.GetRaw(url, nil)
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, apiInfo *dto.SmsPlatformKeyQueueDto) int {
url := fmt.Sprintf("?api_key=%s&action=setStatus&id=%s&status=8", apiInfo.ApiKey, activationId)
client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil)
bytes, _, err := client.GetRaw(url, nil)
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(apiInfo *dto.SmsPlatformKeyQueueDto) ([]dto.DaisysmsPriceResp, error) {
result := make([]dto.DaisysmsPriceResp, 0)
url := fmt.Sprintf("?api_key=%s&action=getPrices", apiInfo.ApiKey)
client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil)
bytes, status, err := client.GetRaw(url, nil)
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, apiInfo *dto.SmsPlatformKeyQueueDto) int {
url := fmt.Sprintf("?api_key=%s&action=setAutoRenew&id=%s&value=%t", apiInfo.ApiKey, activationId, status)
client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.DaisysmsUrl, nil)
bytes, _, err1 := client.GetRaw(url, nil)
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
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
package service

View File

@ -0,0 +1,94 @@
package service
import (
"go-admin/app/admin/service/dto"
"go-admin/common/global"
"go-admin/common/statuscode"
"go-admin/utils/utility"
"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)
}
}
func TestSyncRenewalLogs(t *testing.T) {
db := initSetting()
service := SmsPhone{}
service.Orm = db
service.Log = logger.NewHelper(logger.DefaultLogger)
if err := service.SyncBilingCycleId(); err != nil {
t.Error(err)
}
}
func TestManualRenewal(t *testing.T) {
db := initSetting()
service := SmsRenewalLog{}
service.Orm = db
service.Log = logger.NewHelper(logger.DefaultLogger)
activationId := "lr_01K5DP71G06SFX84S7W99D61F9"
if _, err := service.ManualDeduct(&dto.ManualDeductReq{
ActivationId: activationId,
TradeOrderNo: utility.GenerateTraceID(),
}, 1); err != statuscode.Success {
t.Error(err)
}
}

View File

@ -0,0 +1,314 @@
package service
import (
"errors"
"fmt"
"github.com/go-admin-team/go-admin-core/sdk/service"
"gorm.io/gorm"
"go-admin/app/admin/models"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
cDto "go-admin/common/dto"
"go-admin/utils/utility"
)
type SmsPlatformKey struct {
service.Service
}
// GetPage 获取SmsPlatformKey列表
func (e *SmsPlatformKey) GetPage(c *dto.SmsPlatformKeyGetPageReq, p *actions.DataPermission, list *[]models.SmsPlatformKey, count *int64) error {
var err error
var data models.SmsPlatformKey
err = e.Orm.Model(&data).
Scopes(
cDto.MakeCondition(c.GetNeedSearch()),
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
actions.Permission(data.TableName(), p),
).
Find(list).Limit(-1).Offset(-1).
Count(count).Error
if err != nil {
e.Log.Errorf("SmsPlatformKeyService GetPage error:%s \r\n", err)
return err
}
for index := range *list {
(*list)[index].ApiKey = utility.DesensitizeGeneric((*list)[index].ApiKey, 3, 3, '*')
(*list)[index].ApiSecret = utility.DesensitizeGeneric((*list)[index].ApiSecret, 3, 3, '*')
}
return nil
}
// GetRandomKey 随机获取一个密钥
func (e *SmsPlatformKey) GetRandomKey(platformCode string) (*dto.SmsPlatformKeyQueueDto, error) {
redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log)
return redisService.GetRandomKey(platformCode)
}
// GetRoundRobinKey 轮询获取密钥
func (e *SmsPlatformKey) GetRoundRobinKey(platformCode string) (*dto.SmsPlatformKeyQueueDto, error) {
redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log)
return redisService.GetRoundRobinKey(platformCode)
}
// GetQueueLength 获取队列长度
func (e *SmsPlatformKey) GetQueueLength(platformCode string) (int64, error) {
redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log)
return redisService.GetQueueLength(platformCode)
}
// ClearPlatformQueue 清空指定平台的队列
func (e *SmsPlatformKey) ClearPlatformQueue(platformCode string) error {
redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log)
return redisService.ClearPlatformQueue(platformCode)
}
// Get 获取SmsPlatformKey对象
func (e *SmsPlatformKey) Get(d *dto.SmsPlatformKeyGetReq, p *actions.DataPermission, model *models.SmsPlatformKey) error {
var data models.SmsPlatformKey
err := e.Orm.Model(&data).
Scopes(
actions.Permission(data.TableName(), p),
).
First(model, d.GetId()).Error
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
err = errors.New("查看对象不存在或无权查看")
e.Log.Errorf("Service GetSmsPlatformKey error:%s \r\n", err)
return err
}
if err != nil {
e.Log.Errorf("db error:%s", err)
return err
}
return nil
}
// Insert 创建SmsPlatformKey对象
func (e *SmsPlatformKey) Insert(c *dto.SmsPlatformKeyInsertReq) error {
var err error
var data models.SmsPlatformKey
var count int64
c.Generate(&data)
if err := e.Orm.Model(&models.SmsPlatformKey{}).
Where("platform_code = ? and api_key =? and account=?", data.PlatformCode, data.ApiKey, data.Account).
Count(&count).Error; err != nil {
e.Log.Errorf("SmsPlatformKeyService Insert error:%s \r\n", err)
return err
}
if count > 0 {
return errors.New("ApiKey或账号 已存在")
}
err = e.Orm.Create(&data).Error
if err != nil {
e.Log.Errorf("SmsPlatformKeyService Insert error:%s \r\n", err)
return err
}
if err := e.AddQueque(dto.SmsPlatformKeyQueueDto{
PlatformCode: data.PlatformCode,
Account: data.Account,
ApiKey: data.ApiKey,
ApiSecret: data.ApiSecret,
}); err != nil {
e.Log.Errorf("添加队列失败,%v", err)
return err
}
return nil
}
// Update 修改SmsPlatformKey对象
func (e *SmsPlatformKey) Update(c *dto.SmsPlatformKeyUpdateReq, p *actions.DataPermission) error {
var err error
var data = models.SmsPlatformKey{}
var count int64
e.Orm.Scopes(
actions.Permission(data.TableName(), p),
).First(&data, c.GetId())
oldKey := data.ApiKey
oldSecret := data.ApiSecret
oldStatus := data.Status
oldPlatformCode := data.PlatformCode
if err1 := e.Orm.Model(&models.SmsPlatformKey{}).
Where("platform_code = ? and api_key =? and account=? and id != ?",
data.PlatformCode, data.ApiKey, data.Account, data.Id).
Count(&count).Error; err1 != nil {
e.Log.Errorf("SmsPlatformKeyService Insert error:%s \r\n", err1)
return err1
}
if count > 0 {
return errors.New("ApiKey或账号 已存在")
}
c.Generate(&data)
// 如果要将启用状态改为禁用状态,需要检查该平台是否还有其他启用的密钥
if data.Status == 2 {
var remainingCount int64
err1 := e.Orm.Model(&models.SmsPlatformKey{}).
Where("platform_code = ? AND status = ? AND id != ?", oldPlatformCode, 1, c.GetId()).
Count(&remainingCount).Error
if err1 != nil {
e.Log.Errorf("检查平台剩余启用密钥失败: %v", err1)
return errors.New("检查平台剩余启用密钥失败")
}
if remainingCount == 0 {
return fmt.Errorf("平台 %s 至少需要保留一个启用状态的密钥,无法禁用", oldPlatformCode)
}
}
db := e.Orm.Save(&data)
if err = db.Error; err != nil {
e.Log.Errorf("SmsPlatformKeyService Save error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权更新该数据")
}
if oldStatus == 1 && data.Status == 2 {
if err := e.RemoveQueque(dto.SmsPlatformKeyQueueDto{
PlatformCode: data.PlatformCode,
Account: data.Account,
ApiKey: data.ApiKey,
ApiSecret: data.ApiSecret,
}, false); err != nil {
e.Log.Errorf("删除失败,%v", err)
return err
}
} else if oldStatus == 2 && data.Status == 1 {
if err := e.AddQueque(dto.SmsPlatformKeyQueueDto{
PlatformCode: data.PlatformCode,
Account: data.Account,
ApiKey: data.ApiKey,
ApiSecret: data.ApiSecret,
}); err != nil {
e.Log.Errorf("添加队列失败,%v", err)
return err
}
} else {
oldQueueData := dto.SmsPlatformKeyQueueDto{
PlatformCode: data.PlatformCode,
Account: data.Account,
ApiKey: oldKey,
ApiSecret: oldSecret,
}
queueData := dto.SmsPlatformKeyQueueDto{
PlatformCode: data.PlatformCode,
Account: data.Account,
ApiKey: data.ApiKey,
ApiSecret: data.ApiSecret,
}
if err := e.Replace(oldQueueData, queueData); err != nil {
e.Log.Errorf("替换队列失败,%v", err)
return err
}
}
return nil
}
// Remove 删除SmsPlatformKey
func (e *SmsPlatformKey) Remove(d *dto.SmsPlatformKeyDeleteReq, p *actions.DataPermission) error {
var data models.SmsPlatformKey
var datas []models.SmsPlatformKey
if err1 := e.Orm.Where("id in (?)", d.GetId()).Find(&datas).Error; err1 != nil {
e.Log.Errorf("Service RemoveSmsPlatformKey error:%s \r\n", err1)
return err1
}
var platformCodes []string
for _, item := range datas {
if utility.ContainsString(platformCodes, item.PlatformCode) {
continue
}
platformCodes = append(platformCodes, item.PlatformCode)
}
err := e.Orm.Transaction(func(tx *gorm.DB) error {
db := tx.Model(&data).
Scopes(
actions.Permission(data.TableName(), p),
).Delete(&data, d.GetId())
var count []string
if err1 := tx.Model(&data).Where("status =1").
Group("platform_code").
Select("platform_code").
Scan(&count).
Error; err1 != nil {
e.Log.Errorf("Service RemoveSmsPlatformKey error:%s \r\n", err1)
return err1
}
for _, item := range platformCodes {
if !utility.ContainsString(count, item) {
return errors.New("删除失败,通道最少需要保留一个可用ApiKey")
}
}
if err := db.Error; err != nil {
e.Log.Errorf("Service RemoveSmsPlatformKey error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权删除该数据")
}
return nil
})
if err != nil {
return err
}
for _, item := range datas {
queueDta := dto.SmsPlatformKeyQueueDto{
PlatformCode: item.PlatformCode,
Account: data.Account,
ApiKey: item.ApiKey,
ApiSecret: item.ApiSecret,
}
if err := e.RemoveQueque(queueDta, true); err != nil {
e.Log.Errorf("移出队列失败,%v", err)
return errors.New("删除队列失败")
}
}
return nil
}
// InitQueque 初始化Redis缓存队列
func (e *SmsPlatformKey) InitQueque() error {
redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log)
return redisService.InitRedisQueque()
}
// Replace 替换Redis缓存中的密钥
func (e *SmsPlatformKey) Replace(oldEntity dto.SmsPlatformKeyQueueDto, entity dto.SmsPlatformKeyQueueDto) error {
redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log)
return redisService.ReplaceRedisKey(oldEntity, entity)
}
// RemoveQueque 从Redis缓存中移出队列
func (e *SmsPlatformKey) RemoveQueque(entity dto.SmsPlatformKeyQueueDto, shouldDel bool) error {
redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log)
return redisService.RemoveRedisQueque(entity, shouldDel)
}
// AddQueque 添加到Redis缓存队列
func (e *SmsPlatformKey) AddQueque(entity dto.SmsPlatformKeyQueueDto) error {
redisService := NewSmsPlatformKeyRedis(e.Orm, e.Log)
return redisService.AddRedisQueque(entity)
}

View File

@ -0,0 +1,327 @@
package service
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"time"
"go-admin/app/admin/models"
"go-admin/app/admin/service/dto"
"go-admin/utils/redishelper"
"github.com/bytedance/sonic"
"github.com/go-admin-team/go-admin-core/logger"
"github.com/go-admin-team/go-admin-core/sdk/service"
"gorm.io/gorm"
)
// SmsPlatformKeyRedis Redis版本的SMS平台密钥管理服务
type SmsPlatformKeyRedis struct {
SmsPlatformKey
redisHelper *redishelper.RedisHelper
}
// NewSmsPlatformKeyRedis 创建Redis版本的SMS平台密钥服务
func NewSmsPlatformKeyRedis(orm *gorm.DB, log logger.Logger) *SmsPlatformKeyRedis {
return &SmsPlatformKeyRedis{
SmsPlatformKey: SmsPlatformKey{
Service: service.Service{
Orm: orm,
Log: logger.NewHelper(log),
},
},
redisHelper: redishelper.DefaultRedis,
}
}
// getRedisKey 获取Redis键名
func (e *SmsPlatformKeyRedis) getRedisKey(platformCode string) string {
return fmt.Sprintf("sms:platform:keys:%s", platformCode)
}
// getRedisIndexKey 获取Redis索引键名用于快速查找
func (e *SmsPlatformKeyRedis) getRedisIndexKey(platformCode, apiKey string) string {
return fmt.Sprintf("sms:platform:index:%s:%s", platformCode, apiKey)
}
func (e *SmsPlatformKeyRedis) GetApiInfo(platformCode, apiKey string) (dto.SmsPlatformKeyQueueDto, error) {
indexKey := e.getRedisIndexKey(platformCode, apiKey)
data, err := e.redisHelper.GetString(indexKey)
if err != nil {
e.Log.Errorf("获取Redis索引失败 [%s:%s]: %v", platformCode, apiKey, err)
return dto.SmsPlatformKeyQueueDto{}, err
}
var apiInfo dto.SmsPlatformKeyQueueDto
err = sonic.Unmarshal([]byte(data), &apiInfo)
if err != nil {
e.Log.Errorf("解析Redis数据失败 [%s:%s]: %v", platformCode, apiKey, err)
return dto.SmsPlatformKeyQueueDto{}, err
}
return apiInfo, nil
}
// InitRedisQueque 初始化Redis缓存队列
// 从数据库加载所有SMS平台密钥并存储到Redis中
func (e *SmsPlatformKeyRedis) InitRedisQueque() error {
// 查询所有启用的SMS平台密钥
var list []models.SmsPlatformKey
err := e.Orm.Where("status = ?", 1).Find(&list).Error
if err != nil {
e.Log.Errorf("查询SMS平台密钥失败: %v", err)
return err
}
if len(list) == 0 {
e.Log.Warn("未找到启用的SMS平台密钥")
return nil
}
// 按平台分组
platformKeyMap := make(map[string][]dto.SmsPlatformKeyQueueDto)
for _, item := range list {
data := dto.SmsPlatformKeyQueueDto{
PlatformCode: item.PlatformCode,
Account: item.Account,
ApiKey: item.ApiKey,
ApiSecret: item.ApiSecret,
}
platformKeyMap[item.PlatformCode] = append(platformKeyMap[item.PlatformCode], data)
}
// 将每个平台的密钥存储到Redis
for platformCode, keys := range platformKeyMap {
redisKey := e.getRedisKey(platformCode)
// 清空现有数据
err := e.redisHelper.DeleteString(redisKey)
if err != nil {
e.Log.Errorf("清空Redis队列失败 [%s]: %v", platformCode, err)
}
// 存储密钥列表
for _, key := range keys {
keyJson, _ := json.Marshal(key)
err := e.redisHelper.RPushList(redisKey, string(keyJson))
if err != nil {
e.Log.Errorf("添加密钥到Redis队列失败 [%s]: %v", platformCode, err)
continue
}
// 创建索引,便于快速查找和删除
indexKey := e.getRedisIndexKey(platformCode, key.ApiKey)
err = e.redisHelper.SetString(indexKey, string(keyJson))
if err != nil {
e.Log.Errorf("创建密钥索引失败 [%s:%s]: %v", platformCode, key.ApiKey, err)
}
}
// 设置过期时间24小时
err = e.redisHelper.SetKeyExpiration(redisKey, 24*time.Hour)
if err != nil {
e.Log.Errorf("设置Redis队列过期时间失败 [%s]: %v", platformCode, err)
}
e.Log.Infof("平台 [%s] 初始化 %d 个密钥到Redis缓存", platformCode, len(keys))
}
return nil
}
// AddRedisQueque 添加密钥到Redis缓存
func (e *SmsPlatformKeyRedis) AddRedisQueque(entity dto.SmsPlatformKeyQueueDto) error {
redisKey := e.getRedisKey(entity.PlatformCode)
indexKey := e.getRedisIndexKey(entity.PlatformCode, entity.ApiKey)
// 检查是否已存在
// exists, err := e.redisHelper.GetString(indexKey)
// if err == nil && exists != "" {
// return errors.New("密钥已存在")
// }
// 序列化密钥数据
keyJson, err := json.Marshal(entity)
if err != nil {
return fmt.Errorf("序列化密钥数据失败: %v", err)
}
// 添加到队列
err = e.redisHelper.RPushList(redisKey, string(keyJson))
if err != nil {
return fmt.Errorf("添加密钥到Redis队列失败: %v", err)
}
// 创建索引
err = e.redisHelper.SetString(indexKey, string(keyJson))
if err != nil {
e.Log.Errorf("创建密钥索引失败: %v", err)
// 索引创建失败不影响主要功能
}
// 设置过期时间
err = e.redisHelper.SetKeyExpiration(redisKey, 24*time.Hour)
if err != nil {
e.Log.Errorf("设置Redis队列过期时间失败: %v", err)
}
e.Log.Infof("成功添加密钥到Redis缓存 [%s:%s]", entity.PlatformCode, entity.ApiKey)
return nil
}
// RemoveRedisQueque 从Redis缓存中删除密钥
// shouldDel 是否删除索引
func (e *SmsPlatformKeyRedis) RemoveRedisQueque(entity dto.SmsPlatformKeyQueueDto, shouldDel bool) error {
redisKey := e.getRedisKey(entity.PlatformCode)
indexKey := e.getRedisIndexKey(entity.PlatformCode, entity.ApiKey)
// 获取要删除的密钥数据
keyData, err := e.redisHelper.GetString(indexKey)
if err != nil || keyData == "" {
return errors.New("密钥不存在")
}
// 从队列中删除
_, err = e.redisHelper.LRem(redisKey, keyData)
if err != nil {
return fmt.Errorf("从Redis队列删除密钥失败: %v", err)
}
// 删除索引
if shouldDel {
err = e.redisHelper.DeleteString(indexKey)
if err != nil {
e.Log.Errorf("删除密钥索引失败: %v", err)
// 索引删除失败不影响主要功能
}
}
// e.Log.Infof("成功从Redis缓存删除密钥 [%s:%s]", entity.PlatformCode, entity.ApiKey)
return nil
}
// GetRandomKey 随机获取一个密钥
func (e *SmsPlatformKeyRedis) GetRandomKey(platformCode string) (*dto.SmsPlatformKeyQueueDto, error) {
redisKey := e.getRedisKey(platformCode)
// 获取所有密钥
keys, err := e.redisHelper.GetAllList(redisKey)
if err != nil {
return nil, fmt.Errorf("获取Redis队列失败: %v", err)
}
if len(keys) == 0 {
return nil, errors.New("队列为空")
}
// 随机选择一个
rand.Seed(time.Now().UnixNano())
randomIndex := rand.Intn(len(keys))
keyJson := keys[randomIndex]
var keyDto dto.SmsPlatformKeyQueueDto
err = json.Unmarshal([]byte(keyJson), &keyDto)
if err != nil {
return nil, fmt.Errorf("反序列化密钥数据失败: %v", err)
}
return &keyDto, nil
}
// GetRoundRobinKey 轮询获取密钥
func (e *SmsPlatformKeyRedis) GetRoundRobinKey(platformCode string) (*dto.SmsPlatformKeyQueueDto, error) {
redisKey := e.getRedisKey(platformCode)
// 从队列头部取出一个密钥
keyJson, err := e.redisHelper.LPopList(redisKey)
if err != nil {
return nil, fmt.Errorf("从Redis队列获取密钥失败: %v", err)
}
if keyJson == "" {
return nil, errors.New("队列为空")
}
// 将密钥重新放到队列尾部(实现轮询)
err = e.redisHelper.RPushList(redisKey, keyJson)
if err != nil {
e.Log.Errorf("轮询密钥回放失败: %v", err)
}
var keyDto dto.SmsPlatformKeyQueueDto
err = json.Unmarshal([]byte(keyJson), &keyDto)
if err != nil {
return nil, fmt.Errorf("反序列化密钥数据失败: %v", err)
}
return &keyDto, nil
}
// GetQueueLength 获取队列长度
func (e *SmsPlatformKeyRedis) GetQueueLength(platformCode string) (int64, error) {
redisKey := e.getRedisKey(platformCode)
keys, err := e.redisHelper.GetAllList(redisKey)
if err != nil {
return 0, fmt.Errorf("获取Redis队列长度失败: %v", err)
}
return int64(len(keys)), nil
}
// ReplaceRedisKey 替换Redis缓存中的密钥
func (e *SmsPlatformKeyRedis) ReplaceRedisKey(oldEntity, newEntity dto.SmsPlatformKeyQueueDto) error {
// 先删除旧密钥
err := e.RemoveRedisQueque(oldEntity, true)
if err != nil {
return fmt.Errorf("删除旧密钥失败: %v", err)
}
// 再添加新密钥
err = e.AddRedisQueque(newEntity)
if err != nil {
return fmt.Errorf("添加新密钥失败: %v", err)
}
// e.Log.Infof("成功替换Redis缓存密钥 [%s] %s -> %s", newEntity.PlatformCode, oldEntity.ApiKey, newEntity.ApiKey)
return nil
}
// ClearPlatformQueue 清空指定平台的队列
func (e *SmsPlatformKeyRedis) ClearPlatformQueue(platformCode string) error {
redisKey := e.getRedisKey(platformCode)
err := e.redisHelper.DeleteString(redisKey)
if err != nil {
return fmt.Errorf("清空平台队列失败: %v", err)
}
// 清空相关索引
pattern := fmt.Sprintf("sms:platform:index:%s:*", platformCode)
err = e.redisHelper.DeleteKeysByPrefix(pattern)
if err != nil {
e.Log.Errorf("清空平台索引失败: %v", err)
}
e.Log.Infof("成功清空平台 [%s] 的Redis队列", platformCode)
return nil
}
// 获取平台所有密钥
func (e *SmsPlatformKeyRedis) GetPlatformKeys(platformCode string) ([]dto.SmsPlatformKeyQueueDto, error) {
redisKey := e.getRedisKey(platformCode)
keys, err := e.redisHelper.GetAllList(redisKey)
if err != nil {
return nil, fmt.Errorf("获取Redis队列失败: %v", err)
}
var keyDtos []dto.SmsPlatformKeyQueueDto
for _, keyJson := range keys {
var keyDto dto.SmsPlatformKeyQueueDto
err = json.Unmarshal([]byte(keyJson), &keyDto)
if err != nil {
return nil, fmt.Errorf("反序列化密钥数据失败: %v", err)
}
keyDtos = append(keyDtos, keyDto)
}
return keyDtos, nil
}

View File

@ -0,0 +1,23 @@
package service
import (
"fmt"
"go-admin/common/global"
"testing"
"github.com/go-admin-team/go-admin-core/logger"
)
func TestNextKey(t *testing.T) {
orm := initSetting()
redisService := NewSmsPlatformKeyRedis(orm, logger.DefaultLogger)
val, err := redisService.GetRoundRobinKey(global.SmsPlatformTextVerified)
if err != nil {
t.Errorf("GetRoundRobinKey error:%s \r\n", err)
}
fmt.Printf("val:%s \r\n", val)
}

View File

@ -2,6 +2,7 @@ package service
import (
"errors"
"strconv"
"github.com/go-admin-team/go-admin-core/sdk/service"
"gorm.io/gorm"
@ -38,7 +39,7 @@ func (e SmsReceiveLog) WebHook(req *dto.SmsReceiveWebHookReq) error {
UserId: phoneLog.UserId,
Service: phoneLog.Service,
ServiceCode: phoneLog.ServiceCode,
MessageId: req.MessageID,
MessageId: strconv.Itoa(req.MessageID),
Phone: phoneLog.Phone,
Code: req.Code,
Status: 2,
@ -48,8 +49,16 @@ func (e SmsReceiveLog) WebHook(req *dto.SmsReceiveWebHookReq) error {
return err
}
phoneService := SmsPhone{Service: e.Service}
if code := phoneService.setStatus(req.ActivationID); code != statuscode.Success {
smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log)
apiInfo, err := smsPlatformKeyRedis.GetApiInfo(phoneLog.PlatformCode, phoneLog.ApiKey)
if err != nil {
e.Log.Errorf("获取平台密钥失败, %s", err)
return err
}
phoneService := SmsDaisysms{Service: e.Service}
if code := phoneService.setStatus(req.ActivationID, &apiInfo); code != statuscode.Success {
e.Log.Errorf("接受验证码回调后修改状态失败 %d", code)
}
}

View File

@ -4,6 +4,7 @@ import (
"go-admin/app/admin/service/dto"
"go-admin/config"
"go-admin/utils/redishelper"
"go-admin/utils/utility"
"testing"
"github.com/go-admin-team/go-admin-core/logger"
@ -12,14 +13,21 @@ import (
"gorm.io/gorm"
)
func initSetting() {
dsn := "root:123456@tcp(127.0.0.1:3306)/proxy_server?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
func initSetting() *gorm.DB {
dsn := "root:123456@tcp(127.0.0.1:3306)/proxy_server_prod?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
sdk.Runtime.SetDb("default", db)
// config.ExtConfig.
config.ExtConfig.TrxGridUrl = "https://api.trongrid.io"
config.ExtConfig.DaisysmsUrl = "https://daisysms.com/stubs/handler_api.php"
config.ExtConfig.SmsTextVerified.ApiKey = "ZQ0swXnsaPpeGdwa3c7gT9U9I1Oh9WoDHx0amuYovvaHuqd5u6B4NBBUSUBjR"
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")
utility.InitSnowflake()
return db
}
func TestSmsReceiveLog(t *testing.T) {

View File

@ -2,14 +2,21 @@ package service
import (
"errors"
"fmt"
"time"
"github.com/go-admin-team/go-admin-core/logger"
"github.com/go-admin-team/go-admin-core/sdk/service"
"github.com/shopspring/decimal"
"gorm.io/gorm"
"go-admin/app/admin/models"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
cDto "go-admin/common/dto"
"go-admin/common/global"
"go-admin/common/statuscode"
"go-admin/utils/utility"
)
type SmsRenewalLog struct {
@ -17,22 +24,45 @@ type SmsRenewalLog struct {
}
// GetPage 获取SmsRenewalLog列表
func (e *SmsRenewalLog) GetPage(c *dto.SmsRenewalLogGetPageReq, p *actions.DataPermission, list *[]models.SmsRenewalLog, count *int64) error {
func (e *SmsRenewalLog) GetPage(c *dto.SmsRenewalLogGetPageReq, p *actions.DataPermission, list *[]dto.SmsRenewalLogResp, count *int64) error {
var err error
var data models.SmsRenewalLog
err = e.Orm.Model(&data).
var datas []models.SmsRenewalLog
query := e.Orm.Model(&data).
Joins("left join sys_user on sys_user.user_id = sms_renewal_log.user_id").
Scopes(
cDto.MakeCondition(c.GetNeedSearch()),
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
actions.Permission(data.TableName(), p),
).
Find(list).Limit(-1).Offset(-1).
)
if c.UserName != "" {
query = query.Where("sys_user.username LIKE ?", "%"+c.UserName+"%")
}
err = query.Select("sms_renewal_log.*, sys_user.username").Find(&datas).Limit(-1).Offset(-1).
Count(count).Error
if err != nil {
e.Log.Errorf("SmsRenewalLogService GetPage error:%s \r\n", err)
return err
}
for _, v := range datas {
*list = append(*list, dto.SmsRenewalLogResp{
Id: v.Id,
UserId: v.UserId,
Category: v.Category,
Phone: v.Phone,
Amount: v.Amount,
BeforeTime: v.BeforeTime,
Period: v.Period,
CreatedAt: v.CreatedAt,
UserName: v.Username,
TradeOrderNo: v.TradeOrderNo,
PayOrderNo: v.PayOrderNo,
Status: v.Status,
})
}
return nil
}
@ -107,3 +137,551 @@ func (e *SmsRenewalLog) Remove(d *dto.SmsRenewalLogDeleteReq, p *actions.DataPer
}
return nil
}
// 手动扣费
func (e *SmsRenewalLog) ManualDeduct(req *dto.ManualDeductReq, userId int) (dto.ManualDeductResp, int) {
var entity models.SmsPhone
var result dto.ManualDeductResp
phoneService := SmsPhone{}
phoneService.Orm = e.Orm
phoneService.Log = e.Log
if err := e.Orm.Model(&entity).
Where("activation_id =?", req.ActivationId).
First(&entity).Error; err != nil {
e.Log.Errorf("获取短信号码失败, %s", err)
return result, statuscode.SmsNotExisted
}
// 获取服务配置映射
serviceMap, premiumMap, err := phoneService.getRenewalConfigs()
if err != nil {
e.Log.Errorf("获取短信服务配置失败, %s", err)
return result, statuscode.ServerError
}
var count int64
if err := e.Orm.Model(&models.SmsRenewalLog{}).
Where("phone_id = ? AND status = 1", entity.Id).
Count(&count).Error; err != nil {
e.Log.Errorf("查询号码续费记录失败 [PhoneID: %d]: %v", entity.Id, err)
return result, statuscode.ServerError
}
if count > 0 {
return result, statuscode.SmsRenewalLogExisted
}
if renewLog, err := e.processPhoneRenewal(entity, serviceMap, premiumMap, req.TradeOrderNo, false); err != nil {
e.Log.Errorf("处理号码续费失败 [PhoneID: %d 续费订单号:%s]: %v", entity.Id, renewLog.PayOrderNo, err)
return result, statuscode.ServerError
} else {
result.ActivationId = req.ActivationId
result.TradeOrderNo = renewLog.TradeOrderNo
result.PayOrderNo = renewLog.PayOrderNo
result.BeginTime = req.BeginTime
result.EndTime = renewLog.TargetTime
}
return result, statuscode.Success
}
// 续期详情
func (e *SmsRenewalLog) GetRenewalDetailByTradeOrderNo(req *dto.ManualDeductDetailReq, userId int) (dto.ManualDeductResp, int) {
var entity models.SmsRenewalLog
result := dto.ManualDeductResp{}
if err := e.Orm.Model(entity).
Joins("LEFT JOIN sms_phone s on s.id = sms_renewal_log.phone_id").
Where("sms_renewal_log.trade_order_no =? and sms_renewal_log.user_id =?", req.TradeOrderNo, userId).
Select("sms_renewal_log.*, s.activation_id").
First(&entity).Error; err != nil {
e.Log.Errorf("获取续费记录失败, %s", err)
return result, statuscode.SmsNotExisted
}
result.ActivationId = entity.ActivationId
result.TradeOrderNo = entity.TradeOrderNo
result.PayOrderNo = entity.PayOrderNo
result.BeginTime = entity.BeforeTime
result.EndTime = entity.TargetTime
return result, statuscode.Success
}
// AutoRenewal 自动续期处理
// 处理即将到期的长期号码自动续费逻辑
func (e *SmsRenewalLog) AutoRenewal() error {
if err := e.JudgeRenewalLogStatus(); err != nil {
e.Log.Errorf("处理预扣费记录失败 %v", err)
}
// 获取24小时内到期的自动续费号码
phoneService := SmsPhone{}
phoneService.Orm = e.Orm
phoneService.Log = e.Log
expiredPhones, err := e.getExpiredPhones()
if err != nil {
return err
}
// 获取服务配置映射
serviceMap, premiumMap, err := phoneService.getRenewalConfigs()
if err != nil {
return err
}
// 处理每个到期号码
for _, phone := range expiredPhones {
if renewLog, err := e.processPhoneRenewal(phone, serviceMap, premiumMap, "", false); err != nil {
e.Log.Errorf("处理号码续费失败 [PhoneID: %d 续费订单号:%s]: %v", phone.Id, renewLog.PayOrderNo, err)
continue
}
}
return nil
}
// 手动续费(处理到期号码)
func (e *SmsRenewalLog) ManualRenewal(activationIds []string) ([]string, error) {
if len(activationIds) == 0 {
return nil, errors.New("activationIds 不能为空")
}
var errorIds []string
var expiredPhones []models.SmsPhone
phoneService := SmsPhone{}
phoneService.Orm = e.Orm
phoneService.Log = e.Log
// 查询所有到期号码
if err := e.Orm.Model(&models.SmsPhone{}).
Where("activation_id IN ? ", activationIds). //expire_time < ? time.Now()
Find(&expiredPhones).Error; err != nil {
return nil, err
}
// 获取服务配置映射
serviceMap, premiumMap, err := phoneService.getRenewalConfigs()
if err != nil {
return nil, err
}
// 处理每个到期号码
for _, phone := range expiredPhones {
if renewLog, err := e.processPhoneRenewal(phone, serviceMap, premiumMap, "", true); err != nil {
e.Log.Errorf("处理号码续费失败 [PhoneID: %d 续费订单号:%s]: %v", phone.Id, renewLog.PayOrderNo, err)
errorIds = append(errorIds, fmt.Sprintf("处理号码续费失败 [PhoneID: %d]: %v", phone.Id, err))
continue
}
}
return errorIds, nil
}
// getExpiredPhones 获取即将到期的自动续费号码
func (e *SmsRenewalLog) getExpiredPhones() ([]models.SmsPhone, error) {
var phones []models.SmsPhone
startTime := time.Now().Add(-24 * time.Hour)
endTime := time.Now().Add(24 * time.Hour)
notExistsQuery := e.Orm.Model(&models.SmsRenewalLog{}).
Select("1").
Where("sms_renewal_log.phone_id = sms_phone.id AND sms_renewal_log.status in (1,2) AND status = 1")
err := e.Orm.Model(&models.SmsPhone{}).
Where("auto_renewal = 1 AND type = 1 AND actived = 2 AND expire_time >= ? AND expire_time < ?", startTime, endTime).
Where("NOT EXISTS (?)", notExistsQuery).
Find(&phones).Error
return phones, err
}
// 获取预扣除的记录
func (e *SmsRenewalLog) GetLockRenewalLogs() ([]models.SmsRenewalLog, error) {
var logs []models.SmsRenewalLog
err := e.Orm.Model(&models.SmsRenewalLog{}).
Joins("left join sms_phone s on s.id = sms_renewal_log.phone_id").
Where("sms_renewal_log.status = 1").
Select("sms_renewal_log.*, s.platform_code,s.api_key,s.billing_cycle_id").
Find(&logs).Error
return logs, err
}
// 判断续费是否成功
func (e *SmsRenewalLog) JudgeRenewalLogStatus() error {
logs, err := e.GetLockRenewalLogs()
if err != nil {
return err
}
smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log)
textVerifiedService := SmsTextVerified{Service: e.Service}
for _, item := range logs {
switch item.PlatformCode {
case global.SmsPlatformTextVerified:
apiInfo, err := smsPlatformKeyRedis.GetApiInfo(item.PlatformCode, item.ApiKey)
if err != nil {
e.Log.Errorf("获取短信平台密钥失败 [PlatformCode: %s]: %v", item.PlatformCode, err)
continue
}
cycleInfo, err := textVerifiedService.GetBillingCycle(item.BillingCycleId, &apiInfo)
if err != nil {
e.Log.Errorf("获取短信平台密钥失败 [PlatformCode: %s]: %v", item.PlatformCode, err)
if errors.Is(err, LogNotFund) {
if err := e.rollbackRenewalLog(item.Id); err != nil {
e.Log.Errorf("回滚续费记录失败 [RenewalLogID: %d]: %v", item.Id, err)
}
}
continue
}
startTime := item.BeforeTime.UTC().Truncate(24 * time.Hour)
endTime := cycleInfo.RenewedThrough.Truncate(24 * time.Hour)
// 续费成功
if startTime.Before(endTime) {
if err := e.confirmPayment(item.PhoneId, item.Id); err != nil {
e.Log.Errorf("更新续费记录状态失败 [RenewalLogID: %d]: %v", item.Id, err)
}
} else if item.BeforeTime.Before(time.Now()) {
if err := e.rollbackRenewalLog(item.Id); err != nil {
e.Log.Errorf("回滚续费记录失败 [RenewalLogID: %d]: %v", item.Id, err)
}
}
case global.SmsPlatformDaisysms:
//没有手动续费只能 在到期前直接扣费
timeDuration := time.Since(item.BeforeTime)
if timeDuration < 24*time.Hour {
if err := e.confirmPayment(item.PhoneId, item.Id); err != nil {
e.Log.Errorf("更新续费记录状态失败 [RenewalLogID: %d]: %v", item.Id, err)
}
}
default:
e.Log.Errorf("不支持的短信平台 %s 续费id%d", item.PlatformCode, item.Id)
}
}
return nil
}
// getRenewalConfigs 获取续费相关配置
func (e *SmsPhone) getRenewalConfigs() (map[string]models.SmsServices, map[string]dto.GetSysConfigByKEYForServiceResp, error) {
smsServices := SmsServices{Service: e.Service}
serviceMap := smsServices.GetMapAll()
configService := SysConfig{Service: e.Service}
configKeys := []string{"renew_number_premium_daisysms", "renew_number_premium_textverified"}
premiumMap, err := configService.GetMapByKeys(configKeys)
return serviceMap, premiumMap, err
}
// processPhoneRenewal 处理单个号码的续费
// tradeOrderNo 交易订单号
func (e *SmsRenewalLog) processPhoneRenewal(phone models.SmsPhone, serviceMap map[string]models.SmsServices,
premiumMap map[string]dto.GetSysConfigByKEYForServiceResp, tradeOrderNo string, admin bool) (models.SmsRenewalLog, error) {
var renewLog models.SmsRenewalLog
smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log)
apiInfo, err := smsPlatformKeyRedis.GetApiInfo(phone.PlatformCode, phone.ApiKey)
if err != nil {
return renewLog, fmt.Errorf("获取短信平台密钥失败 [PlatformCode: %s]: %v", phone.PlatformCode, err)
}
//续期id为空 重新获取
if phone.BillingCycleId == "" {
switch phone.PlatformCode {
case global.SmsPlatformTextVerified:
//可以手动续费
textVerifiedService := SmsTextVerified{Service: e.Service}
detail, code := textVerifiedService.GetRentalDetail(phone.ActivationId, &apiInfo)
if code != statuscode.Success {
logger.Errorf("获取短信验证码续费详情失败 [ActivationID: %s]: %v", phone.ActivationId, code)
return renewLog, fmt.Errorf("获取短信验证码续费详情失败 [ActivationID: %s]: %v", phone.ActivationId, code)
}
phone.BillingCycleId = detail.BillingCycleId
case global.SmsPlatformDaisysms:
//只有自动续费的 用定时服务查询状态
}
}
// 获取服务价格
service, exists := serviceMap[phone.PlatformCode+"_"+phone.ServiceCode]
if !exists {
return renewLog, errors.New("服务不存在")
}
// 计算续费价格
renewalPrice := e.calculateRenewalPrice(service.LongPrice, phone.PlatformCode, premiumMap)
if renewalPrice.IsZero() {
return renewLog, errors.New("续费价格计算失败")
}
// 创建续费日志
renewLog = e.createRenewalLog(phone, renewalPrice, tradeOrderNo)
// 执行续费事务
if err := e.executeRenewalTransaction(phone, renewalPrice, &renewLog, &apiInfo, admin); err != nil {
return renewLog, err
}
return renewLog, nil
}
// calculateRenewalPrice 计算续费价格
func (e *SmsRenewalLog) calculateRenewalPrice(basePrice decimal.Decimal, platformCode string, premiumMap map[string]dto.GetSysConfigByKEYForServiceResp) decimal.Decimal {
percent := decimal.NewFromInt(1)
var configKey string
switch platformCode {
case global.SmsPlatformDaisysms:
configKey = "renew_number_premium_daisysms"
case global.SmsPlatformTextVerified:
configKey = "renew_number_premium_textverified"
default:
return basePrice
}
if config, exists := premiumMap[configKey]; exists {
if val, err := decimal.NewFromString(config.ConfigValue); err == nil && val.Cmp(decimal.Zero) > 0 {
percent = decimal.NewFromInt(100).Add(val).Div(decimal.NewFromInt(100))
}
}
return basePrice.Mul(percent).Truncate(2)
}
// createRenewalLog 创建续费日志
// tradeOrderNo 交易订单号
func (e *SmsRenewalLog) createRenewalLog(phone models.SmsPhone, amount decimal.Decimal, tradeOrderNo string) models.SmsRenewalLog {
targetTime := phone.ExpireTime.AddDate(0, 0, 30)
return models.SmsRenewalLog{
UserId: phone.UserId,
TradeOrderNo: tradeOrderNo,
PayOrderNo: utility.GenerateTraceID(),
Phone: phone.Phone,
Category: 2,
PhoneId: phone.Id,
Type: phone.Type,
Amount: amount,
BeforeTime: *phone.ExpireTime,
TargetTime: &targetTime,
Period: 30,
Status: 1,
}
}
// executeRenewalTransaction 执行续费事务
func (e *SmsRenewalLog) executeRenewalTransaction(phone models.SmsPhone, price decimal.Decimal,
renewLog *models.SmsRenewalLog, apiInfo *dto.SmsPlatformKeyQueueDto, isAdmin bool) error {
err := e.Orm.Transaction(func(tx *gorm.DB) error {
// 扣除余额
result := tx.Exec("UPDATE member_balance SET balance = balance - ? WHERE user_id = ? AND balance >= ?", price, phone.UserId, price)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("余额不足")
}
// 创建续费日志
if err := tx.Create(renewLog).Error; err != nil {
return errors.New("创建续费日志失败")
}
return nil
})
if err != nil && err.Error() == "余额不足" && phone.ExpireTime.Before(time.Now()) {
return e.handleInsufficientBalance(phone)
}
//是否是管理员处理
if isAdmin {
if err1 := e.confirmPayment(phone.Id, renewLog.Id); err1 != nil {
logger.Errorf("确认扣款失败 [PhoneID: %d, RenewalLogID: %d]: %v", phone.Id, renewLog.Id, err1)
return err1
}
} else {
switch phone.PlatformCode {
case global.SmsPlatformTextVerified:
//可以手动续费
textVerifiedService := SmsTextVerified{Service: e.Service}
code := textVerifiedService.ManualRenewal(phone.BillingCycleId, apiInfo)
// 没有需要续费的号码 重新设置可自动续费之后再试一次
if code == statuscode.NothingToRenew {
if renewCode := textVerifiedService.Renew(phone.ActivationId, true, apiInfo); renewCode == statuscode.Success {
code = textVerifiedService.ManualRenewal(phone.BillingCycleId, apiInfo)
}
}
if code == statuscode.Success {
if err1 := e.confirmPayment(phone.Id, renewLog.Id); err1 != nil {
logger.Errorf("确认扣款失败 [PhoneID: %d, RenewalLogID: %d]: %v", phone.Id, renewLog.Id, err1)
return err1
}
} else {
if err1 := e.rollbackRenewalLog(renewLog.Id); err1 != nil {
logger.Errorf("回滚续费日志状态失败续费日志ID: %d, 错误: %v", renewLog.Id, err1)
return err1
}
return fmt.Errorf("手动续费失败 [BillingCycleID: %s]: %v", phone.BillingCycleId, code)
}
case global.SmsPlatformDaisysms:
//只有自动续费的 用定时服务查询状态
}
}
return err
}
// 确认扣款成功
func (e *SmsRenewalLog) confirmPayment(phoneId int, renewalLogId int) error {
var renewLog models.SmsRenewalLog
if err := e.Orm.Where("id = ?", renewalLogId).First(&renewLog).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 事务ID不存在这是致命错误平台A必须有此记录才能Confirm
e.Log.Errorf("Confirm失败: 找不到续费记录ID %s 的续费日志", renewalLogId)
return errors.New("事务记录不存在,无法确认支付")
}
return fmt.Errorf("查询续费日志失败: %w", err)
}
// 检查幂等性
if renewLog.Status == 2 { // 状态 2: 扣费成功
// 幂等性成功:已经被 Confirm 过了,直接返回成功,不需重复操作
e.Log.Warnf("Confirm重复调用: 续费ID %s 已是成功状态", renewalLogId)
return nil
}
// 流程错误检查:不能对已取消或失败的事务执行 Confirm
if renewLog.Status != 1 { // 状态 1: 预扣中/冻结
// 理论上不该发生。说明平台 B 的调用时序错误。
e.Log.Errorf("Confirm失败: 续费ID %s 状态为 %d无法执行确认支付", renewalLogId, renewLog.Status)
return errors.New("事务状态不正确,无法确认支付")
}
return e.Orm.Transaction(func(tx *gorm.DB) error {
// 1. 查找并更新续费日志状态 (状态 2: 扣费成功)
result := tx.Model(&models.SmsRenewalLog{}).
Where("id = ? AND status = ?", renewalLogId, 1). // 必须是预扣中状态
Update("status", 2)
if result.RowsAffected == 0 {
return errors.New("续费日志状态不正确或已处理")
}
// 2. **更新到期时间** (只有在 Confirm 阶段才能更新)
var phone models.SmsPhone
if err := tx.Model(&models.SmsPhone{}).Where("id = ?", phoneId).First(&phone).Error; err != nil {
return err
}
var targetTime time.Time
if renewLog.TargetTime != nil {
targetTime = *renewLog.TargetTime
} else {
targetTime = phone.ExpireTime.AddDate(0, 0, 30)
}
if err := tx.Model(&phone).Update("expire_time", targetTime).Error; err != nil {
return err
}
return nil // TCC Confirm 成功
})
}
// rollbackRenewalLog 回滚续费日志状态
func (e *SmsRenewalLog) rollbackRenewalLog(renewLogId int) error {
var renewLog models.SmsRenewalLog
if err := e.Orm.Where("id = ?", renewLogId).First(&renewLog).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
e.Log.Errorf("回滚失败: 找不到续费记录ID %d", renewLogId)
return errors.New("事务记录不存在,无法回滚")
}
return fmt.Errorf("查询续费日志失败: %w", err)
}
// 幂等性检查:如果已经处于回滚状态,直接返回成功
if renewLog.Status == 3 {
e.Log.Warnf("回滚重复调用: 续费ID %d 已是回滚状态 (Status=3)", renewLogId)
return nil // 幂等性成功
}
// 流程检查:如果已经是成功扣费状态,不能执行普通回滚
if renewLog.Status == 2 {
e.Log.Errorf("回滚失败: 续费ID %d 已是成功扣费状态 (Status=2)", renewLogId)
return errors.New("事务已确认支付,无法执行取消回滚")
}
return e.Orm.Transaction(func(tx *gorm.DB) error {
// 1. 更新续费日志状态 (状态 1: 预扣中/冻结)
result := tx.Model(&models.SmsRenewalLog{}).
Where("id = ? AND status = ?", renewLogId, 1). // 必须是扣费成功状态
Update("status", 3)
if result.Error != nil {
logger.Errorf("回滚续费日志状态失败续费日志ID: %d, 错误: %v", renewLogId, result.Error)
return result.Error
}
if result.RowsAffected == 0 {
logger.Errorf("回滚续费日志状态失败续费日志ID: %d", renewLogId)
return errors.New("续费日志状态不正确或已处理")
}
result = tx.Exec("UPDATE member_balance SET balance = balance + ? WHERE user_id = ? ", renewLog.Amount, renewLog.UserId)
if result.Error != nil {
logger.Errorf("回滚续费日志状态失败续费日志ID: %d, 错误: %v", renewLogId, result.Error)
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("余额不足")
}
return nil
})
}
// handleInsufficientBalance 处理余额不足情况
func (e *SmsRenewalLog) handleInsufficientBalance(phone models.SmsPhone) error {
params := map[string]interface{}{
"auto_renewal": 2,
"remark": "余额不足,取消自动续费",
}
// 取消自动续费
if err := e.Orm.Model(&models.SmsPhone{}).
Where("id = ?", phone.Id).
Updates(params).Error; err != nil {
e.Log.Errorf("余额不足,取消自动续费失败: %v", err)
}
phoneService := SmsPhone{}
phoneService.Orm = e.Orm
phoneService.Log = e.Log
// 调用平台取消续费接口
code := phoneService.ChangeAutoRenewManage(phone.PlatformCode, phone.ApiKey, phone.ActivationId, false)
if code != statuscode.Success {
params["auto_renewal"] = 1
params["remark"] = ""
// 如果平台取消失败,恢复自动续费状态
if err := e.Orm.Model(&models.SmsPhone{}).
Where("id = ?", phone.Id).
Updates(params).Error; err != nil {
e.Log.Errorf("恢复自动续费状态失败: %v", err)
}
e.Log.Errorf("平台取消自动续费失败,状态码: %d", code)
}
return errors.New("余额不足")
}

View File

@ -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,119 @@ 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",
"renew_number_premium_daisysms", "renew_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,
PlatformCode: item.PlatformCode,
Price: item.Price,
LongPrice: item.LongPrice,
RenewLongPrice: 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)
}
}
if config, ok := mapConfigs["renew_number_premium_daisysms"]; ok {
premium, err := decimal.NewFromString(config.ConfigValue)
if err != nil || premium.IsZero() {
e.Log.Errorf("浮动百分比为0,或者是转换错误 %s", config.ConfigValue)
}
respItem.RenewLongPrice = respItem.RenewLongPrice.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)
}
}
//续期价格浮动
if config, ok := mapConfigs["renew_number_premium_textverified"]; ok {
premium, err := decimal.NewFromString(config.ConfigValue)
if err != nil || premium.IsZero() {
e.Log.Errorf("浮动百分比为0,或者是转换错误 %s", config.ConfigValue)
}
respItem.RenewLongPrice = respItem.RenewLongPrice.Mul(decimal.NewFromInt(100).Add(premium).Div(decimal.NewFromInt(100))).Truncate(2)
}
}
*resp = append(*resp, respItem)
}
return nil
@ -66,13 +210,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 +302,48 @@ 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
}
// 获取服务map
// map key ={platformCode}_{serviceCode}
func (e *SmsServices) GetMapAll() map[string]models.SmsServices {
var datas []models.SmsServices
if err := e.Orm.Model(&models.SmsServices{}).Find(&datas).Error; err != nil {
e.Log.Errorf("SmsServicesService GetMapAll error:%s \r\n", err)
return nil
}
mapServices := make(map[string]models.SmsServices)
for _, item := range datas {
mapServices[item.PlatformCode+"_"+item.Code] = item
}
return mapServices
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,224 @@
package service
import (
"errors"
"fmt"
"go-admin/app/admin/service/dto"
"go-admin/common/global"
"go-admin/common/statuscode"
"go-admin/config"
"go-admin/utils/httphelper"
"go-admin/utils/redishelper"
"net/http"
"strings"
"time"
"github.com/go-admin-team/go-admin-core/sdk/service"
"github.com/go-redis/redis/v8"
)
// SmsTextVerifiedEnhanced 增强版TextVerified服务支持token自动刷新
type SmsTextVerifiedEnhanced struct {
service.Service
maxRetries int // 最大重试次数
}
// NewSmsTextVerifiedEnhanced 创建增强版TextVerified服务实例
func NewSmsTextVerifiedEnhanced() *SmsTextVerifiedEnhanced {
return &SmsTextVerifiedEnhanced{
maxRetries: 2, // 默认最大重试2次
}
}
// AuthenticatedRequest 带自动token刷新的HTTP请求包装器
// 当遇到401未授权错误时会自动刷新token并重试请求
func (e *SmsTextVerifiedEnhanced) AuthenticatedRequest(
apiInfo *dto.SmsPlatformKeyQueueDto,
requestFunc func(client *httphelper.HTTPClient) (int, error),
) (int, error) {
var lastErr error
// 尝试执行请求,包含重试逻辑
for attempt := 0; attempt <= e.maxRetries; attempt++ {
// 获取认证客户端
client, code := e.GetTextVerifiedAuthClient(apiInfo)
if code != statuscode.Success {
e.Log.Errorf("获取授权客户端失败,状态码: %d", code)
return code, fmt.Errorf("获取授权客户端失败,状态码: %d", code)
}
// 执行请求
status, err := requestFunc(client)
// 如果请求成功,直接返回
if err == nil {
return status, nil
}
// 检查是否为401未授权错误
if e.isUnauthorizedError(status, err) {
e.Log.Warnf("检测到token过期或未授权错误 (尝试 %d/%d): %v", attempt+1, e.maxRetries+1, err)
// 如果不是最后一次尝试清除token缓存并重试
if attempt < e.maxRetries {
if clearErr := e.clearTokenCache(apiInfo); clearErr != nil {
e.Log.Errorf("清除token缓存失败: %v", clearErr)
}
e.Log.Infof("正在重试请求... (尝试 %d/%d)", attempt+2, e.maxRetries+1)
continue
}
}
// 如果不是401错误或已达到最大重试次数记录错误并退出循环
lastErr = err
break
}
// 返回最后一次的错误
return statuscode.ServerError, fmt.Errorf("请求失败,已重试%d次: %v", e.maxRetries, lastErr)
}
// isUnauthorizedError 检查错误是否为401未授权错误
func (e *SmsTextVerifiedEnhanced) isUnauthorizedError(status int, err error) bool {
if status == http.StatusUnauthorized {
return true
}
if err != nil {
errorMsg := strings.ToLower(err.Error())
// 检查常见的未授权错误消息
return strings.Contains(errorMsg, "unauthorized") ||
strings.Contains(errorMsg, "401") ||
strings.Contains(errorMsg, "invalid token") ||
strings.Contains(errorMsg, "token expired") ||
strings.Contains(errorMsg, "authentication failed")
}
return false
}
// clearTokenCache 清除Redis中的token缓存
func (e *SmsTextVerifiedEnhanced) clearTokenCache(apiInfo *dto.SmsPlatformKeyQueueDto) error {
key := fmt.Sprintf(global.TextVerifiedToken, apiInfo.ApiKey)
err := redishelper.DefaultRedis.DeleteString(key)
if err != nil {
return fmt.Errorf("删除token缓存失败: %v", err)
}
e.Log.Infof("已清除token缓存: %s", key)
return nil
}
// GetTextVerifiedAuthClient 获取认证的HTTP客户端
// 这个方法与原版相同,但可以在这里添加额外的逻辑
func (e *SmsTextVerifiedEnhanced) GetTextVerifiedAuthClient(apiInfo *dto.SmsPlatformKeyQueueDto) (*httphelper.HTTPClient, int) {
header, err := e.GetAuthHeader(apiInfo)
if err != nil {
e.Log.Errorf("获取授权头失败: %v", err)
return nil, statuscode.ServerError
}
client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.SmsTextVerified.Url, header)
return client, statuscode.Success
}
// GetAuthHeader 获取认证头
func (e *SmsTextVerifiedEnhanced) GetAuthHeader(apiInfo *dto.SmsPlatformKeyQueueDto) (map[string]string, error) {
token, err := e.GetToken(apiInfo)
if err != nil {
return nil, err
}
headers := map[string]string{
"Authorization": "Bearer " + token,
}
return headers, nil
}
// GetToken 获取token与原版相同的逻辑
func (e *SmsTextVerifiedEnhanced) GetToken(apiInfo *dto.SmsPlatformKeyQueueDto) (string, error) {
key := fmt.Sprintf(global.TextVerifiedToken, apiInfo.ApiKey)
token, err := redishelper.DefaultRedis.GetString(key)
if err != nil && errors.Is(err, redis.Nil) {
// token不存在重新登录获取
return e.Login(apiInfo)
}
if token == "" {
return e.Login(apiInfo)
}
return token, nil
}
// Login 登录获取token与原版相同的逻辑
func (e *SmsTextVerifiedEnhanced) Login(apiInfo *dto.SmsPlatformKeyQueueDto) (string, error) {
resp := dto.TextVerifiedLoginResp{}
var token string
client := httphelper.NewHTTPClient(10*time.Second, config.ExtConfig.SmsTextVerified.Url, nil)
headers := map[string]string{
"X-API-USERNAME": apiInfo.ApiSecret,
"X-API-KEY": apiInfo.ApiKey,
}
_, err1 := client.Post("/api/pub/v2/auth", nil, headers, &resp)
if err1 != nil {
e.Log.Errorf("TextVerified登录失败 error: %v", err1)
return "", err1
}
if resp.Token == "" {
e.Log.Errorf("TextVerified登录失败返回的Token为空")
return "", errors.New("TextVerified登录失败返回的Token为空")
}
token = resp.Token
if resp.ExpiresIn >= 10 {
resp.ExpiresIn = resp.ExpiresIn - 10 // 提前10秒过期
key := fmt.Sprintf(global.TextVerifiedToken, apiInfo.ApiKey)
if err := redishelper.DefaultRedis.SetStringExpire(key, token, time.Duration(resp.ExpiresIn)*time.Second); err != nil {
e.Log.Errorf("TextVerified登录失败缓存Token失败 error: %v", err)
}
}
return token, nil
}
// 使用示例增强版的GetCode方法
func (e *SmsTextVerifiedEnhanced) GetCodeEnhanced(messageId string, typ int, userId int, service, serviceCode string, apiInfo *dto.SmsPlatformKeyQueueDto) (string, int) {
reservationType := ""
if typ == 0 {
reservationType = "verification"
} else {
reservationType = "renewable"
}
url := fmt.Sprintf("/api/pub/v2/sms?reservationId=%s&reservationType=%s", messageId, reservationType)
var parsedCode string
var finalStatus int
// 使用增强的请求方法
status, err := e.AuthenticatedRequest(apiInfo, func(client *httphelper.HTTPClient) (int, error) {
resp := dto.TextVerifiedSmsResp{}
status, err := client.Get(url, nil, &resp)
if err != nil {
return status, err
}
// 处理响应数据...
if len(resp.Data) > 0 {
// 这里可以添加原有的验证码处理逻辑
parsedCode = resp.Data[0].ParsedCode
}
return http.StatusOK, nil
})
if err != nil {
e.Log.Errorf("获取验证码失败: %v", err)
finalStatus = statuscode.ServerError
} else {
finalStatus = status
}
return parsedCode, finalStatus
}

View File

@ -0,0 +1,84 @@
package service
import (
"go-admin/common/global"
"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)
smsPlatformKeyRedis := NewSmsPlatformKeyRedis(s.Orm, s.Log)
apiInfo, err := smsPlatformKeyRedis.GetRoundRobinKey(global.SmsPlatformTextVerified)
if err != nil {
s.Log.Errorf("获取API信息失败: %v", err)
t.Error("获取API信息失败", err)
}
token, err := s.Login(apiInfo)
if err != nil {
t.Errorf("Login failed: %v", err)
} else {
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)
smsPlatformKeyRedis := NewSmsPlatformKeyRedis(s.Orm, s.Log)
apiInfo, err := smsPlatformKeyRedis.GetRoundRobinKey(global.SmsPlatformTextVerified)
if err != nil {
s.Log.Errorf("获取API信息失败: %v", err)
t.Error("获取API信息失败", err)
}
// Now, test GetServices with the valid token
servicesResp, err := s.GetServices(apiInfo)
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")
}
}
func TestInitSmsLogs(t *testing.T) {
db := initSetting()
s := SmsTextVerified{}
s.Orm = db
s.Log = logger.NewHelper(logger.DefaultLogger)
err := s.InitSmsLogs()
if err != nil {
t.Errorf("InitSmsLogs failed: %v", err)
} else {
t.Log("InitSmsLogs succeeded")
}
}

View File

@ -162,6 +162,22 @@ func (e *SysConfig) Remove(d *dto.SysConfigDeleteReq) error {
return nil
}
func (e *SysConfig) GetMapByKeys(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
}
// GetWithKey 根据Key获取SysConfig
func (e *SysConfig) GetWithKey(c *dto.SysConfigByKeyReq, resp *dto.GetSysConfigByKEYForServiceResp) error {
var err error
@ -175,6 +191,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.

View File

@ -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

View File

@ -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
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
}

View File

@ -19,6 +19,7 @@ func InitJob() {
"SmsJob": SmsJob{}, //短信定时查询验证码
"SmsRenewalJob": SmsRenewalJob{}, //短信定时自动续期
"AutoDeleteJob": AutoDeleteJob{}, //定时删除任务
"SmsPriceJob": SmsPriceJob{}, // 短信价格定时同步
// ...
}
}

View File

@ -37,11 +37,11 @@ func (j RenewalJob) Exec(args interface{}) error {
// 定时短信续期任务
func (j SmsRenewalJob) Exec(args interface{}) error {
smsService := service.SmsServices{}
smsService.Orm = GetDb()
smsService.Log = logger.NewHelper(logger.DefaultLogger)
smsPhoneService := service.SmsRenewalLog{}
smsPhoneService.Orm = GetDb()
smsPhoneService.Log = logger.NewHelper(logger.DefaultLogger)
return smsService.AutoRenewal()
return smsPhoneService.AutoRenewal()
}
// 过期任务

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -23,10 +23,14 @@ func TestTrxJob(t *testing.T) {
}
func initSetting() {
dsn := "root:123456@tcp(127.0.0.1:3306)/proxy_server?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
dsn := "root:123456@tcp(127.0.0.1:3306)/proxy_server_prod?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")
}

View File

@ -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,18 @@ func initBusinesses() {
if _, err := cliProxyService.GetTrafficInfo(); err != nil {
os.Exit(-1)
}
memberApiService := service.MemberApi{}
memberApiService.Orm = cliProxyService.Orm
memberApiService.Log = cliProxyService.Log
memberApiService.InitApis()
// 创建SMS平台密钥服务实例并初始化Redis缓存
smsPlatformKey := service.SmsPlatformKey{}
smsPlatformKey.Orm = cliProxyService.Orm
smsPlatformKey.Log = cliProxyService.Log
// 初始化Redis缓存队列
smsPlatformKey.InitQueque()
}

Binary file not shown.

View File

@ -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" //接码过期记录保留时长

View File

@ -0,0 +1,7 @@
package global
//短效平台code 字典【sms_platform】
const (
SmsPlatformDaisysms = "daisysms"
SmsPlatformTextVerified = "textverified"
)

View File

@ -0,0 +1,6 @@
package global
const (
// TextVerified Token {apiKey}
TextVerifiedToken = "TextVerifiedToken:%s"
)

View File

@ -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
@ -34,3 +49,107 @@ func AuthInit() (*jwt.GinJWTMiddleware, error) {
})
}
// 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
}

View File

@ -0,0 +1,6 @@
package rediskey
const (
//用户api
MemberApiKey = "member_api:%s"
)

View File

@ -0,0 +1,6 @@
package rediskey
const (
//平台key
SmsPlatformKey = "sms_platform_key:%s"
)

View File

@ -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,6 +25,9 @@ var StatusCodeZh = map[int]string{
MaxPriceExceeded: "超过最大接受单价",
NoNumbers: "号码不足",
RentalsNotFinished: "需要先完成部分租赁才能继续租赁",
Unauthorized: "未授权",
ApiUnActived: "API未激活",
InvalidParams: "参数错误",
SmsCancel: "短信验证码_手机号过期",
SmsNoActivation: "短信验证码_手机号不存在",
@ -26,6 +37,12 @@ var StatusCodeZh = map[int]string{
SmsNotExpired: "号码未过期无法删除",
SmsNotAutoRenew: "短效号码无法自动续期",
SmsServiceUnavailable: "%s服务暂不可用",
SmsPlatformUnavailable: "通道不可用",
SmsInvalidType: "短信验证码_无效类型",
SmsOutOfStockOrUnavailable: "短信验证码_缺货或服务不可用",
SmsRentalRefundNotPermitted: "短信验证码_租赁退款不允许",
SmsRentalCantRenew: "短信验证码_无法续期",
SmsRenewalLogExisted: "短信验证码_重复续期",
}
var StatusCodeEn = map[int]string{
@ -43,6 +60,9 @@ 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",
@ -52,6 +72,12 @@ var StatusCodeEn = map[int]string{
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",
SmsRentalCantRenew: "sms rental can not renewal",
SmsRenewalLogExisted: "sms renewal log existed",
}
// GetMsg 获取状态码对应的消息
@ -85,7 +111,8 @@ const (
AccountOrPasswordError = 10003
//密码不一致
PassWordNotMatch = 10004
// 没有需要续费的号码 重新设置可自动续费之后再试一次
NothingToRenew = 11005
//
ServerError = 500
@ -108,6 +135,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 +158,16 @@ const (
SmsNotAutoRenew = 20020
//短信验证码_服务暂不可用
SmsServiceUnavailable = 20021
//短信-通道不可用
SmsPlatformUnavailable = 20022
//短信-未知类型
SmsInvalidType = 20023
// 短信-缺货或服务不可用
SmsOutOfStockOrUnavailable = 20024
// 短信-租赁退款不允许
SmsRentalRefundNotPermitted = 20025
// 短信-无法续期
SmsRentalCantRenew = 20026
// 短信-重复续期
SmsRenewalLogExisted = 20028
)

View File

@ -12,9 +12,17 @@ var ExtConfig Extend
type Extend struct {
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 {

View File

@ -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"
#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

6
go.mod
View File

@ -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

View File

@ -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 传递 nilrawResponse 传递 &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 传递 nilrawResponse 传递 &raw
statusCode, err := c.DoRequest(http.MethodPost, path, nil, customHeaders, nil, &raw)
if err != nil {
return nil, statusCode, err
}
return raw, statusCode, nil
}

View File

@ -0,0 +1,221 @@
package utility
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
)
// 加密key
var CryptoKey = "ProxyServer@#(123321)!Keycrypto"
// CryptoHelper 加密帮助类
type CryptoHelper struct {
key []byte
}
// NewCryptoHelper 创建新的加密帮助实例
// key: 32字节的加密密钥如果长度不足会自动填充超出会截断
func NewCryptoHelper(key string) *CryptoHelper {
// 确保密钥长度为32字节AES-256
keyBytes := make([]byte, 32)
copy(keyBytes, []byte(key))
return &CryptoHelper{
key: keyBytes,
}
}
// Encrypt 加密字符串
// plaintext: 要加密的明文
// 返回: base64编码的密文和错误信息
func (c *CryptoHelper) Encrypt(plaintext string) (string, error) {
if plaintext == "" {
return "", errors.New("plaintext cannot be empty")
}
// 创建AES cipher
block, err := aes.NewCipher(c.key)
if err != nil {
return "", fmt.Errorf("failed to create cipher: %w", err)
}
// 使用GCM模式
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", fmt.Errorf("failed to create GCM: %w", err)
}
// 生成随机nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", fmt.Errorf("failed to generate nonce: %w", err)
}
// 加密数据
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
// 返回base64编码的结果
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt 解密字符串
// ciphertext: base64编码的密文
// 返回: 解密后的明文和错误信息
func (c *CryptoHelper) Decrypt(ciphertext string) (string, error) {
if ciphertext == "" {
return "", errors.New("ciphertext cannot be empty")
}
// base64解码
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", fmt.Errorf("failed to decode base64: %w", err)
}
// 创建AES cipher
block, err := aes.NewCipher(c.key)
if err != nil {
return "", fmt.Errorf("failed to create cipher: %w", err)
}
// 使用GCM模式
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", fmt.Errorf("failed to create GCM: %w", err)
}
// 检查数据长度
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return "", errors.New("ciphertext too short")
}
// 提取nonce和密文
nonce, cipherData := data[:nonceSize], data[nonceSize:]
// 解密数据
plaintext, err := gcm.Open(nil, nonce, cipherData, nil)
if err != nil {
return "", fmt.Errorf("failed to decrypt: %w", err)
}
return string(plaintext), nil
}
// EncryptBytes 加密字节数组
// data: 要加密的字节数组
// 返回: 加密后的字节数组和错误信息
func (c *CryptoHelper) EncryptBytes(data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, errors.New("data cannot be empty")
}
// 创建AES cipher
block, err := aes.NewCipher(c.key)
if err != nil {
return nil, fmt.Errorf("failed to create cipher: %w", err)
}
// 使用GCM模式
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("failed to create GCM: %w", err)
}
// 生成随机nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, fmt.Errorf("failed to generate nonce: %w", err)
}
// 加密数据
ciphertext := gcm.Seal(nonce, nonce, data, nil)
return ciphertext, nil
}
// DecryptBytes 解密字节数组
// ciphertext: 加密后的字节数组
// 返回: 解密后的字节数组和错误信息
func (c *CryptoHelper) DecryptBytes(ciphertext []byte) ([]byte, error) {
if len(ciphertext) == 0 {
return nil, errors.New("ciphertext cannot be empty")
}
// 创建AES cipher
block, err := aes.NewCipher(c.key)
if err != nil {
return nil, fmt.Errorf("failed to create cipher: %w", err)
}
// 使用GCM模式
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("failed to create GCM: %w", err)
}
// 检查数据长度
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return nil, errors.New("ciphertext too short")
}
// 提取nonce和密文
nonce, cipherData := ciphertext[:nonceSize], ciphertext[nonceSize:]
// 解密数据
plaintext, err := gcm.Open(nil, nonce, cipherData, nil)
if err != nil {
return nil, fmt.Errorf("failed to decrypt: %w", err)
}
return plaintext, nil
}
// GenerateKey 生成随机密钥
// 返回: 32字节的随机密钥字符串
func GenerateKey() (string, error) {
key := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
return "", fmt.Errorf("failed to generate key: %w", err)
}
return base64.StdEncoding.EncodeToString(key), nil
}
// QuickEncrypt 快速加密函数(使用默认密钥)
// plaintext: 要加密的明文
// key: 加密密钥
// 返回: base64编码的密文和错误信息
func QuickEncrypt(plaintext, key string) (string, error) {
crypto := NewCryptoHelper(key)
return crypto.Encrypt(plaintext)
}
// QuickDecrypt2 快速解密函数(使用默认密钥)
// ciphertext: base64编码的密文
// 返回: 解密后的明文和错误信息
func QuickDecrypt2(ciphertext string) (string, error) {
crypto := NewCryptoHelper(CryptoKey)
return crypto.Decrypt(ciphertext)
}
// QuickEncrypt2 快速加密函数(使用默认密钥)
// plaintext: 要加密的明文
// 返回: base64编码的密文和错误信息
func QuickEncrypt2(plaintext string) (string, error) {
crypto := NewCryptoHelper(CryptoKey)
return crypto.Encrypt(plaintext)
}
// QuickDecrypt 快速解密函数(使用默认密钥)
// ciphertext: base64编码的密文
// key: 解密密钥
// 返回: 解密后的明文和错误信息
func QuickDecrypt(ciphertext, key string) (string, error) {
crypto := NewCryptoHelper(key)
return crypto.Decrypt(ciphertext)
}

View File

@ -0,0 +1,371 @@
package utility
import (
"sync"
"time"
)
// GenericQueue 通用循环队列
// 支持存储任意类型的数据,实现负载均衡和容错
type GenericQueue[T any] struct {
data []T // 存储数据的数组
current int // 当前指针位置
size int // 队列中的元素数量
capacity int // 队列容量
mutex sync.RWMutex // 读写锁,保证线程安全
lastUsed map[int]time.Time // 记录每个位置的最后使用时间
cooldown time.Duration // 冷却时间,避免频繁使用同一个元素
comparer func(T, T) bool // 比较函数,用于检查重复元素
}
var (
// QuequeMap 全局队列映射表,用于管理多个命名队列
// 使用interface{}类型以支持不同泛型类型的队列
QuequeMap = make(map[string]interface{})
)
// NewGenericQueue 创建新的通用循环队列
// capacity: 队列容量
// comparer: 比较函数,用于检查重复元素(可选)
// cooldown: 元素使用冷却时间可选默认0表示无冷却
func NewGenericQueue[T any](capacity int, comparer func(T, T) bool, cooldown ...time.Duration) *GenericQueue[T] {
cd := time.Duration(0)
if len(cooldown) > 0 {
cd = cooldown[0]
}
return &GenericQueue[T]{
data: make([]T, capacity),
capacity: capacity,
lastUsed: make(map[int]time.Time),
cooldown: cd,
comparer: comparer,
}
}
// Add 添加元素到队列
// item: 要添加的元素
// 返回: 是否添加成功
func (q *GenericQueue[T]) Add(item T) bool {
q.mutex.Lock()
defer q.mutex.Unlock()
if q.size >= q.capacity {
return false // 队列已满
}
// 如果提供了比较函数,检查是否已存在相同的元素
if q.comparer != nil {
for i := 0; i < q.size; i++ {
if q.comparer(q.data[i], item) {
return false // 已存在
}
}
}
q.data[q.size] = item
q.size++
return true
}
// Remove 从队列中移除指定的元素
// item: 要移除的元素
// 返回: 是否移除成功
func (q *GenericQueue[T]) Remove(item T) bool {
q.mutex.Lock()
defer q.mutex.Unlock()
if q.comparer == nil {
return false // 没有比较函数无法移除
}
for i := 0; i < q.size; i++ {
if q.comparer(q.data[i], item) {
// 将后面的元素前移
for j := i; j < q.size-1; j++ {
q.data[j] = q.data[j+1]
}
q.size--
// 调整current指针
if q.current >= q.size && q.size > 0 {
q.current = 0
}
// 清理lastUsed记录
delete(q.lastUsed, i)
return true
}
}
return false
}
// GetNext 获取下一个可用的元素(轮询方式)
// 返回: 元素和是否获取成功
func (q *GenericQueue[T]) GetNext() (T, bool) {
q.mutex.Lock()
defer q.mutex.Unlock()
var zero T
if q.size == 0 {
return zero, false // 队列为空
}
// 如果没有设置冷却时间,直接返回下一个元素
if q.cooldown == 0 {
item := q.data[q.current]
q.lastUsed[q.current] = time.Now()
q.current = (q.current + 1) % q.size
return item, true
}
// 寻找可用的元素(考虑冷却时间)
startPos := q.current
for {
lastUsed, exists := q.lastUsed[q.current]
// 如果元素从未使用过,或者已过冷却时间
if !exists || time.Since(lastUsed) >= q.cooldown {
item := q.data[q.current]
q.lastUsed[q.current] = time.Now()
q.current = (q.current + 1) % q.size
return item, true
}
q.current = (q.current + 1) % q.size
// 如果遍历了一圈都没找到可用的元素
if q.current == startPos {
// 返回当前元素,忽略冷却时间
item := q.data[q.current]
q.lastUsed[q.current] = time.Now()
q.current = (q.current + 1) % q.size
return item, true
}
}
}
// GetRandom 随机获取一个元素
// 返回: 元素和是否获取成功
func (q *GenericQueue[T]) GetRandom() (T, bool) {
q.mutex.RLock()
defer q.mutex.RUnlock()
var zero T
if q.size == 0 {
return zero, false
}
// 使用当前时间作为随机种子
index := int(time.Now().UnixNano()) % q.size
item := q.data[index]
q.lastUsed[index] = time.Now()
return item, true
}
// GetAll 获取所有元素的副本
// 返回: 元素切片
func (q *GenericQueue[T]) GetAll() []T {
q.mutex.RLock()
defer q.mutex.RUnlock()
items := make([]T, q.size)
copy(items, q.data[:q.size])
return items
}
// Size 获取队列中的元素数量
// 返回: 元素数量
func (q *GenericQueue[T]) Size() int {
q.mutex.RLock()
defer q.mutex.RUnlock()
return q.size
}
// IsEmpty 检查队列是否为空
// 返回: 是否为空
func (q *GenericQueue[T]) IsEmpty() bool {
q.mutex.RLock()
defer q.mutex.RUnlock()
return q.size == 0
}
// IsFull 检查队列是否已满
// 返回: 是否已满
func (q *GenericQueue[T]) IsFull() bool {
q.mutex.RLock()
defer q.mutex.RUnlock()
return q.size >= q.capacity
}
// Clear 清空队列
func (q *GenericQueue[T]) Clear() {
q.mutex.Lock()
defer q.mutex.Unlock()
q.size = 0
q.current = 0
q.lastUsed = make(map[int]time.Time)
}
// SetCooldown 设置元素使用冷却时间
// cooldown: 冷却时间
func (q *GenericQueue[T]) SetCooldown(cooldown time.Duration) {
q.mutex.Lock()
defer q.mutex.Unlock()
q.cooldown = cooldown
}
// GetUsageInfo 获取元素使用信息
// 返回: 位置使用时间映射
func (q *GenericQueue[T]) GetUsageInfo() map[int]time.Time {
q.mutex.RLock()
defer q.mutex.RUnlock()
usage := make(map[int]time.Time)
for k, v := range q.lastUsed {
usage[k] = v
}
return usage
}
// BatchAdd 批量添加元素
// items: 要添加的元素切片
// 返回: 成功添加的数量
func (q *GenericQueue[T]) BatchAdd(items []T) int {
count := 0
for _, item := range items {
if q.Add(item) {
count++
}
}
return count
}
// Replace 替换所有元素
// items: 新的元素切片
// 返回: 是否替换成功
func (q *GenericQueue[T]) Replace(items []T) bool {
if len(items) > q.capacity {
return false
}
q.mutex.Lock()
defer q.mutex.Unlock()
q.size = 0
q.current = 0
q.lastUsed = make(map[int]time.Time)
for _, item := range items {
q.data[q.size] = item
q.size++
}
return true
}
// ReplaceItem 替换指定的单个元素
// oldItem: 要被替换的元素
// newItem: 新的元素
// 返回: 是否替换成功
func (q *GenericQueue[T]) ReplaceItem(oldItem, newItem T) bool {
q.mutex.Lock()
defer q.mutex.Unlock()
if q.comparer == nil {
return false // 没有比较函数无法查找元素
}
for i := 0; i < q.size; i++ {
if q.comparer(q.data[i], oldItem) {
q.data[i] = newItem
return true
}
}
return false // 未找到要替换的元素
}
// Enqueue 入队操作(队列尾部添加元素)
// item: 要添加的元素
// 返回: 是否添加成功
func (q *GenericQueue[T]) Enqueue(item T) bool {
return q.Add(item)
}
// Dequeue 出队操作(从队列头部移除并返回元素)
// 返回: 元素和是否成功
func (q *GenericQueue[T]) Dequeue() (T, bool) {
q.mutex.Lock()
defer q.mutex.Unlock()
var zero T
if q.size == 0 {
return zero, false // 队列为空
}
// 获取队列头部元素
item := q.data[0]
// 将后面的元素前移
for i := 0; i < q.size-1; i++ {
q.data[i] = q.data[i+1]
}
q.size--
// 调整current指针
if q.current > 0 {
q.current--
}
if q.current >= q.size && q.size > 0 {
q.current = 0
}
// 重新映射lastUsed因为索引发生了变化
newLastUsed := make(map[int]time.Time)
for index, lastTime := range q.lastUsed {
if index > 0 {
newLastUsed[index-1] = lastTime
}
}
q.lastUsed = newLastUsed
return item, true
}
// Peek 查看队列头部元素(不移除)
// 返回: 元素和是否成功
func (q *GenericQueue[T]) Peek() (T, bool) {
q.mutex.RLock()
defer q.mutex.RUnlock()
var zero T
if q.size == 0 {
return zero, false // 队列为空
}
return q.data[0], true
}
// PeekLast 查看队列尾部元素(不移除)
// 返回: 元素和是否成功
func (q *GenericQueue[T]) PeekLast() (T, bool) {
q.mutex.RLock()
defer q.mutex.RUnlock()
var zero T
if q.size == 0 {
return zero, false // 队列为空
}
return q.data[q.size-1], true
}
// ApiKeyInfo API密钥信息结构体
type ApiKeyInfo struct {
Key string `json:"key"` // API密钥
Name string `json:"name"` // 密钥名称
Weight int `json:"weight"` // 权重
Enabled bool `json:"enabled"` // 是否启用
Metadata map[string]string `json:"metadata"` // 元数据
}