diff --git a/app/admin/apis/sms_phone.go b/app/admin/apis/sms_phone.go index 1c377fa..5f33317 100644 --- a/app/admin/apis/sms_phone.go +++ b/app/admin/apis/sms_phone.go @@ -752,7 +752,7 @@ func (e SmsPhone) OpenWeakUp(c *gin.Context) { // 手动续费 func (e SmsPhone) ManualRenewal(c *gin.Context) { req := dto.ManualRenewalReq{} - s := service.SmsPhone{} + s := service.SmsRenewalLog{} err := e.MakeContext(c). MakeOrm(). diff --git a/app/admin/apis/sms_renewal_log.go b/app/admin/apis/sms_renewal_log.go index fd91344..454d2ad 100644 --- a/app/admin/apis/sms_renewal_log.go +++ b/app/admin/apis/sms_renewal_log.go @@ -194,9 +194,9 @@ func (e SmsRenewalLog) Delete(c *gin.Context) { } // 手动扣费 -func (e SmsPhone) ManualDeduct(c *gin.Context) { +func (e SmsRenewalLog) ManualDeduct(c *gin.Context) { req := dto.ManualDeductReq{} - s := service.SmsPhone{} + s := service.SmsRenewalLog{} err := e.MakeContext(c). MakeOrm(). @@ -217,12 +217,45 @@ func (e SmsPhone) ManualDeduct(c *gin.Context) { return } - _, err = s.ManualDeduct(&req, userId) + 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 } - e.OK(nil, "success") + 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") } diff --git a/app/admin/models/sms_phone.go b/app/admin/models/sms_phone.go index 4bedcbd..db7348c 100644 --- a/app/admin/models/sms_phone.go +++ b/app/admin/models/sms_phone.go @@ -30,6 +30,7 @@ type SmsPhone struct { 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 diff --git a/app/admin/models/sms_renewal_log.go b/app/admin/models/sms_renewal_log.go index 31ef843..b2de4d6 100644 --- a/app/admin/models/sms_renewal_log.go +++ b/app/admin/models/sms_renewal_log.go @@ -11,15 +11,24 @@ import ( type SmsRenewalLog struct { models.Model - PhoneId int `json:"phoneId" gorm:"type:bigint;comment:号码id"` - Phone string `json:"phone" gorm:"type:varchar(50);comment:号码"` - 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:续费前过期时间"` - Period int `json:"period" gorm:"type:int;comment:时间段"` - Username string `json:"username" gorm:"->"` + 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 } diff --git a/app/admin/router/sms_renewal_log.go b/app/admin/router/sms_renewal_log.go index fdf0e3a..cdfb0e9 100644 --- a/app/admin/router/sms_renewal_log.go +++ b/app/admin/router/sms_renewal_log.go @@ -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) @@ -24,4 +32,4 @@ func registerSmsRenewalLogRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWT r.PUT("/:id", actions.PermissionAction(), api.Update) r.DELETE("", api.Delete) } -} \ No newline at end of file +} diff --git a/app/admin/service/dto/sms_phone.go b/app/admin/service/dto/sms_phone.go index 2299131..f03383a 100644 --- a/app/admin/service/dto/sms_phone.go +++ b/app/admin/service/dto/sms_phone.go @@ -192,12 +192,13 @@ type SmsPhoneCleanMyPhoneReq struct { } 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"` + 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 { @@ -235,4 +236,23 @@ type BatchCancelAbnormalNumberReq struct { 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"` } diff --git a/app/admin/service/dto/sms_renewal_log.go b/app/admin/service/dto/sms_renewal_log.go index 47c127b..f789d6c 100644 --- a/app/admin/service/dto/sms_renewal_log.go +++ b/app/admin/service/dto/sms_renewal_log.go @@ -112,16 +112,19 @@ func (s *SmsRenewalLogDeleteReq) GetId() interface{} { } type SmsRenewalLogResp struct { - Id int `json:"id" comment:"主键id"` // 主键id - PhoneId int `json:"phoneId" comment:"号码id"` - Phone string `json:"phone" 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:"创建时间"` + 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:"用户名"` @@ -129,5 +132,21 @@ type SmsRenewalLogResp struct { // 手动扣费 type ManualDeductReq struct { - ActivationId string `json:"activationId" comment:"激活id"` + 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:"交易订单号"` } diff --git a/app/admin/service/dto/text_verified.go b/app/admin/service/dto/text_verified.go index c064898..76c8cde 100644 --- a/app/admin/service/dto/text_verified.go +++ b/app/admin/service/dto/text_verified.go @@ -160,16 +160,17 @@ type VerificationDTO struct { // 长效详情 type VerificationRentalDetailResp struct { - Number string `json:"number"` - SMS TextVerifiedResp `json:"sms"` - Calls TextVerifiedResp `json:"calls"` - CreatedAt time.Time `json:"createdAt"` - ID string `json:"id"` - Sale TextVerifiedResp `json:"sale"` - SaleId string `json:"saleId"` - ServiceName string `json:"serviceName"` - State string `json:"state"` - AlwaysOn bool `json:"alwaysOn"` + 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"` } // 包含可执行标志 + 链接 @@ -221,6 +222,38 @@ type TextVerifiedGetNumbersResp struct { 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"` +} diff --git a/app/admin/service/sms_phone.go b/app/admin/service/sms_phone.go index d697031..1a1c592 100644 --- a/app/admin/service/sms_phone.go +++ b/app/admin/service/sms_phone.go @@ -17,30 +17,13 @@ import ( cDto "go-admin/common/dto" "go-admin/common/global" "go-admin/common/statuscode" + "go-admin/utils/utility" ) type SmsPhone struct { service.Service } -// 手动扣费 -func (e *SmsPhone) ManualDeduct(req *dto.ManualDeductReq, userId int) (any, int) { - //TODO - var entity models.SmsPhone - - if err := e.Orm.Model(&entity).Where("activation_id =?", req.ActivationId).First(&entity).Error; err != nil { - e.Log.Errorf("获取短信号码失败, %s", err) - return nil, statuscode.SmsNotExisted - } - - var serviceMap map[string]models.SmsServices - var permiumMap map[string]dto.GetSysConfigByKEYForServiceResp - - e.processPhoneRenewal(entity, serviceMap, permiumMap) - - return nil, statuscode.Success -} - // open-API 获取电话号码 func (e *SmsPhone) OpenGetNumber(req *dto.GetNumberReq, userId int) (dto.OpenGetNumberResp, int) { resp := dto.OpenGetNumberResp{} @@ -339,7 +322,7 @@ func (e *SmsPhone) DoGetNumber(balanceService *MemberBalance, req *dto.GetNumber } now := time.Now() - activationId, phone, code, expireTime, startTime, endTime, apiInfo := e.GetNumberManage(req.PlatformCode, req.Type, req.ServiceCode, price, req.Period) + numberResp, code, apiInfo := e.GetNumberManage(req.PlatformCode, req.Type, req.ServiceCode, price, req.Period) if code != statuscode.Success { return decimal.Decimal{}, models.SmsPhone{}, code @@ -347,24 +330,25 @@ func (e *SmsPhone) DoGetNumber(balanceService *MemberBalance, req *dto.GetNumber smsPhone := models.SmsPhone{} smsPhone.PlatformCode = req.PlatformCode - smsPhone.Phone = phone + smsPhone.Phone = numberResp.Phone smsPhone.UserId = userId smsPhone.Service = serviceItem.Name smsPhone.ServiceCode = req.ServiceCode smsPhone.Type = req.Type smsPhone.Period = req.Period - smsPhone.ActivationId = activationId - smsPhone.MessageId = activationId - smsPhone.NewActivationId = activationId + smsPhone.ActivationId = numberResp.ActivationId + smsPhone.MessageId = numberResp.ActivationId + smsPhone.NewActivationId = numberResp.ActivationId smsPhone.Actived = 1 - smsPhone.StartTime = startTime - smsPhone.EndTime = endTime + smsPhone.StartTime = numberResp.StartTime + smsPhone.EndTime = numberResp.EndTime smsPhone.ApiKey = apiInfo.ApiKey - + smsPhone.BillingCycleId = numberResp.BillingCycleId smsPhone.Price = price + if req.Type == 1 { - if expireTime != nil { - smsPhone.ExpireTime = expireTime + if numberResp.ExpireTime != nil { + smsPhone.ExpireTime = numberResp.ExpireTime } else { days := req.Period * 30 now = now.AddDate(0, 0, days) @@ -376,8 +360,8 @@ func (e *SmsPhone) DoGetNumber(balanceService *MemberBalance, req *dto.GetNumber smsPhone.Actived = 2 } } else { - if expireTime != nil { - smsPhone.ExpireTime = expireTime + if numberResp.ExpireTime != nil { + smsPhone.ExpireTime = numberResp.ExpireTime } else { now = now.Add(time.Minute * time.Duration(serviceItem.ExpirationMinutes)) smsPhone.ExpireTime = &now @@ -418,6 +402,7 @@ func (e *SmsPhone) DoGetNumber(balanceService *MemberBalance, req *dto.GetNumber log := models.SmsRenewalLog{} log.PhoneId = smsPhone.Id + log.PayOrderNo = utility.GenerateTraceID() log.Phone = smsPhone.Phone log.Type = smsPhone.Type log.Category = 1 @@ -425,6 +410,8 @@ func (e *SmsPhone) DoGetNumber(balanceService *MemberBalance, req *dto.GetNumber log.Amount = price log.BeforeTime = time.Now() log.Period = req.Period * 30 + log.Status = 2 + log.TargetTime = smsPhone.ExpireTime if err := e.Orm.Save(&log).Error; err != nil { e.Log.Errorf("保存短信续费日志失败,phone_id:%d, %s", smsPhone.Id, err) @@ -441,31 +428,44 @@ func (e *SmsPhone) DoGetNumber(balanceService *MemberBalance, req *dto.GetNumber // *startTime 长效号码单次接码开始使用(textverified有用) // *endTime 长效号码单次接码过期时间(textverified有用) func (e *SmsPhone) GetNumberManage(platformCode string, typ int, serviceCode string, - price decimal.Decimal, period int) (string, string, int, *time.Time, *time.Time, *time.Time, *dto.SmsPlatformKeyQueueDto) { + price decimal.Decimal, period int) (dto.SmsPhoneGetNumberResp, int, *dto.SmsPlatformKeyQueueDto) { smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log) queue, err := smsPlatformKeyRedis.GetRoundRobinKey(platformCode) + result := dto.SmsPhoneGetNumberResp{} + if err != nil { e.Log.Errorf("获取短信平台队列失败, %s", err) - return "", "", statuscode.ServerError, nil, nil, nil, queue + return result, statuscode.ServerError, queue } switch platformCode { case global.SmsPlatformDaisysms: service := SmsDaisysms{Service: e.Service} activationId, phone, code := service.GetNumberForApi(queue, typ, serviceCode, price, period) + result.ActivationId = strconv.Itoa(activationId) + result.Phone = phone + result.ExpireTime = nil + result.StartTime = nil + result.EndTime = nil - return strconv.Itoa(activationId), phone, code, nil, nil, nil, queue + return result, code, queue case global.SmsPlatformTextVerified: service := SmsTextVerified{Service: e.Service} resp, code := service.GetNumberAndWakeUp(typ, serviceCode, price, period, queue) + result.ActivationId = resp.Id + result.Phone = resp.Phone + result.ExpireTime = resp.EndAt + result.StartTime = resp.StartTime + result.EndTime = resp.EndTime + result.BillingCycleId = resp.BillingCycleId if code != statuscode.Success { - return "", "", code, nil, resp.StartTime, resp.EndTime, queue + return result, code, queue } - return resp.Id, resp.Phone, code, resp.EndAt, resp.StartTime, resp.EndTime, queue + return result, code, queue default: - return "", "", statuscode.SmsPlatformUnavailable, nil, nil, nil, queue + return result, statuscode.SmsPlatformUnavailable, queue } } @@ -605,217 +605,6 @@ func (e *SmsPhone) GetCodeManage(platformCode string, messageId string, typ int, } } -// AutoRenewal 自动续期处理 -// 处理即将到期的长期号码自动续费逻辑 -func (e *SmsPhone) AutoRenewal() error { - // 获取24小时内到期的自动续费号码 - expiredPhones, err := e.getExpiredPhones() - if err != nil { - return err - } - - // 获取服务配置映射 - serviceMap, premiumMap, err := e.getRenewalConfigs() - if err != nil { - return err - } - - // 处理每个到期号码 - for _, phone := range expiredPhones { - if err := e.processPhoneRenewal(phone, serviceMap, premiumMap); err != nil { - e.Log.Errorf("处理号码续费失败 [PhoneID: %d]: %v", phone.Id, err) - continue - } - } - - return nil -} - -// 手动续费(处理到期号码) -func (e *SmsPhone) ManualRenewal(activationIds []string) ([]string, error) { - if len(activationIds) == 0 { - return nil, errors.New("activationIds 不能为空") - } - - var errorIds []string - var expiredPhones []models.SmsPhone - - // 查询所有到期号码 - if err := e.Orm.Model(&models.SmsPhone{}). - Where("activation_id IN ? and expire_time < ?", activationIds, time.Now()). - Find(&expiredPhones).Error; err != nil { - return nil, err - } - - // 获取服务配置映射 - serviceMap, premiumMap, err := e.getRenewalConfigs() - if err != nil { - return nil, err - } - - // 处理每个到期号码 - for _, phone := range expiredPhones { - if err := e.processPhoneRenewal(phone, serviceMap, premiumMap); err != nil { - e.Log.Errorf("处理号码续费失败 [PhoneID: %d]: %v", phone.Id, err) - errorIds = append(errorIds, fmt.Sprintf("处理号码续费失败 [PhoneID: %d]: %v", phone.Id, err)) - continue - } - } - return errorIds, nil -} - -// getExpiredPhones 获取即将到期的自动续费号码 -func (e *SmsPhone) getExpiredPhones() ([]models.SmsPhone, error) { - var phones []models.SmsPhone - startTime := time.Now().Add(-24 * time.Hour) - endTime := time.Now().Add(24 * time.Hour) - - err := e.Orm.Model(&models.SmsPhone{}). - Where("auto_renewal = 1 AND type = 1 AND actived = 2 AND expire_time >= ? AND expire_time < ?", startTime, endTime). - Find(&phones).Error - - return phones, err -} - -// 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 处理单个号码的续费 -func (e *SmsPhone) processPhoneRenewal(phone models.SmsPhone, serviceMap map[string]models.SmsServices, premiumMap map[string]dto.GetSysConfigByKEYForServiceResp) error { - // 获取服务价格 - service, exists := serviceMap[phone.PlatformCode+"_"+phone.ServiceCode] - if !exists { - return errors.New("服务不存在") - } - - // 计算续费价格 - renewalPrice := e.calculateRenewalPrice(service.LongPrice, phone.PlatformCode, premiumMap) - if renewalPrice.IsZero() { - return errors.New("续费价格计算失败") - } - - // 创建续费日志 - renewLog := e.createRenewalLog(phone, renewalPrice) - - // 执行续费事务 - return e.executeRenewalTransaction(phone, renewalPrice, renewLog) -} - -// calculateRenewalPrice 计算续费价格 -func (e *SmsPhone) 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 创建续费日志 -func (e *SmsPhone) createRenewalLog(phone models.SmsPhone, amount decimal.Decimal) models.SmsRenewalLog { - return models.SmsRenewalLog{ - UserId: phone.UserId, - Phone: phone.Phone, - Category: 2, - PhoneId: phone.Id, - Type: phone.Type, - Amount: amount, - BeforeTime: *phone.ExpireTime, - Period: 30, - } -} - -// executeRenewalTransaction 执行续费事务 -func (e *SmsPhone) executeRenewalTransaction(phone models.SmsPhone, price decimal.Decimal, renewLog models.SmsRenewalLog) 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("创建续费日志失败") - } - - // 更新到期时间 - newExpireTime := phone.ExpireTime.AddDate(0, 0, 30) - params := map[string]interface{}{ - "expire_time": newExpireTime, - "remark": "", - } - - if err := tx.Model(&models.SmsPhone{}).Where("id = ?", phone.Id). - Updates(params).Error; err != nil { - return err - } - - return nil - }) - - if err != nil && err.Error() == "余额不足" && phone.ExpireTime.Before(time.Now()) { - return e.handleInsufficientBalance(phone) - } - - return err -} - -// handleInsufficientBalance 处理余额不足情况 -func (e *SmsPhone) 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) - } - - // 调用平台取消续费接口 - code := e.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("余额不足") -} - // ChangeAutoRenew 修改自动续期 func (e *SmsPhone) ChangeAutoRenew(req *dto.SmsPhoneChangeAutoRenewReq, userId int) int { var data models.SmsPhone @@ -843,8 +632,9 @@ func (e *SmsPhone) ChangeAutoRenew(req *dto.SmsPhoneChangeAutoRenewReq, userId i configService := SysConfig{Service: e.Service} configKeys := []string{"renew_number_premium_daisysms", "renew_number_premium_textverified"} premiumMap, _ := configService.GetMapByKeys(configKeys) + renewalService := SmsRenewalLog{Service: e.Service} - price := e.calculateRenewalPrice(serviceItem.LongPrice, serviceItem.PlatformCode, premiumMap) + price := renewalService.calculateRenewalPrice(serviceItem.LongPrice, serviceItem.PlatformCode, premiumMap) if balance.Cmp(price) < 0 { return statuscode.BalanceNotEnough @@ -1053,3 +843,71 @@ func (e *SmsPhone) DeleteExpired() error { return nil } + +// 同步手动续费id +func (e *SmsPhone) SyncBilingCycleId() error { + var logs []models.SmsPhone + logMap := map[string]models.SmsPhone{} + if err := e.Orm.Model(&models.SmsPhone{}).Where("billing_cycle_id = '' or billing_cycle_id is null").Find(&logs).Error; err != nil { + e.Log.Errorf("查询手动续费id为空的记录失败 err:%v", err) + return err + } + + for _, item := range logs { + logMap[item.ActivationId] = item + } + + textVerifiedService := SmsTextVerified{Service: e.Service} + smsPlatformKeyRedis := NewSmsPlatformKeyRedis(e.Orm, e.Log) + apiInfo, err := smsPlatformKeyRedis.GetApiInfo(global.SmsPlatformTextVerified, "ZQ0swXnsaPpeGdwa3c7gT9U9I1Oh9WoDHx0amuYovvaHuqd5u6B4NBBUSUBjR") + + if err != nil { + e.Log.Errorf("获取平台密钥失败: %v", err) + return err + } + + _, err = e.syncLoop("", textVerifiedService, apiInfo, logMap) + if err != nil { + return err + } + + return nil +} + +func (e *SmsPhone) syncLoop(nextUrl string, textVerifiedService SmsTextVerified, apiInfo dto.SmsPlatformKeyQueueDto, logMap map[string]models.SmsPhone) (dto.TextVerifiedGetRentalListResp, error) { + if len(logMap) == 0 { + return dto.TextVerifiedGetRentalListResp{}, nil + } + + rentalList, err := textVerifiedService.GetRentalList(nextUrl, &apiInfo) + + if err != nil { + e.Log.Errorf("获取平台密钥失败: %v", err) + return dto.TextVerifiedGetRentalListResp{}, err + } + + for _, log := range rentalList.Data { + if log.BillingCycleId == "" { + continue + } + + if item, ok := logMap[log.ID]; ok { + if err1 := e.Orm.Model(&models.SmsPhone{}).Where("activation_id = ?", item.ActivationId).Update("billing_cycle_id", log.BillingCycleId).Error; err1 != nil { + e.Log.Errorf("更新手动续费id失败 err:%v", err1) + continue + } + + delete(logMap, item.ActivationId) + } + } + + if rentalList.HasNext { + time.Sleep(150 * time.Millisecond) + rentalList, err = e.syncLoop(rentalList.Links.Next.Href, textVerifiedService, apiInfo, logMap) + + if err != nil { + e.Log.Errorf("同步续费id失败 err:%v", err) + } + } + return rentalList, nil +} diff --git a/app/admin/service/sms_phone_test.go b/app/admin/service/sms_phone_test.go index 0404979..96c51d4 100644 --- a/app/admin/service/sms_phone_test.go +++ b/app/admin/service/sms_phone_test.go @@ -4,6 +4,7 @@ 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" @@ -63,3 +64,31 @@ func TestGetPrices(t *testing.T) { 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) + } +} diff --git a/app/admin/service/sms_receive_log_test.go b/app/admin/service/sms_receive_log_test.go index 4d6b6e6..afa9417 100644 --- a/app/admin/service/sms_receive_log_test.go +++ b/app/admin/service/sms_receive_log_test.go @@ -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" @@ -13,7 +14,7 @@ import ( ) func initSetting() *gorm.DB { - 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. @@ -25,6 +26,7 @@ func initSetting() *gorm.DB { redishelper.InitDefaultRedis("127.0.0.1:6379", "", 4) redishelper.InitLockRedisConn("127.0.0.1:6379", "", "4") + utility.InitSnowflake() return db } diff --git a/app/admin/service/sms_renewal_log.go b/app/admin/service/sms_renewal_log.go index 81e6de5..1635eb3 100644 --- a/app/admin/service/sms_renewal_log.go +++ b/app/admin/service/sms_renewal_log.go @@ -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 { @@ -42,15 +49,18 @@ func (e *SmsRenewalLog) GetPage(c *dto.SmsRenewalLogGetPageReq, p *actions.DataP 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, + 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 @@ -127,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("余额不足") +} diff --git a/app/admin/service/sms_text_verified.go b/app/admin/service/sms_text_verified.go index 9a05d4e..3d9f984 100644 --- a/app/admin/service/sms_text_verified.go +++ b/app/admin/service/sms_text_verified.go @@ -29,6 +29,32 @@ type SmsTextVerified struct { service.Service } +// 错误 +var ( + LogNotFund = errors.New("记录不存在") +) + +const ( + loginUrl = "/api/pub/v2/auth" + getAreas = "/api/pub/v2/area-codes" + getServices = "/api/pub/v2/services" + createRental = "/api/pub/v2/reservations/rental" //长效收码 + createVerification = "/api/pub/v2/verifications" //单次收码 + cancelVerification = "/api/pub/v2/verifications/%s/cancel" //取消收码 + cancelRental = "/api/pub/v2/reservations/rental/renewable/%s/refund" //取消长效号码 + wakeUp = "/api/pub/v2/wake-requests" //唤醒号码 + rentalPrice = "/api/pub/v2/pricing/rentals" //长效收码价格 + verificationPrice = "/api/pub/v2/pricing/verifications" //单次收码价格 + rentalDetail = "/api/pub/v2/reservations/rental/renewable/%s" //长效号码详情 + renewRental = "/api/pub/v2/reservations/rental/renewable/%s/renew" //长效续期 + updateRentalRenewStatus = "/api/pub/v2/reservations/rental/renewable/%s" // 更改续租状态 + getSmsCode = "/api/pub/v2/sms?reservationId=%s&reservationType=%s" //获取短信验证码 + getNumbers = "/api/pub/v2/reservations/rental/renewable" //获取长效号码列表 Get + manualRenewal = "/api/pub/v2/billing-cycles/%s/renew" //手动续期 POST + getBillingCycles = "/api/pub/v2/billing-cycles/%s" //获取计费周期详情 GET + rentalList = "/api/pub/v2/reservations/rental/renewable" //长效号码列表 +) + // 获取收到的验证码 // messageId 短效或长效号码的ID // typ 0-短效 1-长效 @@ -143,24 +169,6 @@ func (e SmsTextVerified) GetCode(messageId string, typ int, userId int, service, return parsedCode, http.StatusOK } -const ( - loginUrl = "/api/pub/v2/auth" - getAreas = "/api/pub/v2/area-codes" - getServices = "/api/pub/v2/services" - createRental = "/api/pub/v2/reservations/rental" //长效收码 - createVerification = "/api/pub/v2/verifications" //单次收码 - cancelVerification = "/api/pub/v2/verifications/%s/cancel" //取消收码 - cancelRental = "/api/pub/v2/reservations/rental/renewable/%s/refund" //取消长效号码 - wakeUp = "/api/pub/v2/wake-requests" //唤醒号码 - rentalPrice = "/api/pub/v2/pricing/rentals" //长效收码价格 - verificationPrice = "/api/pub/v2/pricing/verifications" //单次收码价格 - rentalDetail = "/api/pub/v2/reservations/rental/renewable/%s" //长效号码详情 - renewRental = "/api/pub/v2/reservations/rental/renewable/%s/renew" //长效续期 - updateRentalRenewStatus = "/api/pub/v2/reservations/rental/renewable/%s" // 更改续租状态 - getSmsCode = "/api/pub/v2/sms?reservationId=%s&reservationType=%s" //获取短信验证码 - getNumbers = "/api/pub/v2/reservations/rental/renewable" //获取长效号码列表 Get -) - var idPattern = regexp.MustCompile(`^[a-zA-Z0-9_]{28}$`) // 获取授权失败 @@ -469,6 +477,7 @@ func (e *SmsTextVerified) GetNumberForApi(typ int, serviceCode string, price dec } result.Phone = "1" + detail.Number + result.BillingCycleId = detail.BillingCycleId endTime := resp.UpdatedAt.AddDate(0, 0, 30) result.EndAt = &(endTime) // 30天后过期 @@ -940,6 +949,41 @@ func (e *SmsTextVerified) Renew(activationId string, status bool, apiInfo *dto.S } } +// 手动续期 +// 续费id +func (e *SmsTextVerified) ManualRenewal(bilingCycleId string, apiInfo *dto.SmsPlatformKeyQueueDto) int { + url := fmt.Sprintf(manualRenewal, bilingCycleId) + + client, code := e.GetTextVerifiedAuthClient(apiInfo) + + if code != statuscode.Success { + e.Log.Errorf("获取长效续期失败 %d", code) + return statuscode.ServerError + } + + headers := map[string]string{ + "Content-Type": "application/json", + } + mapData := dto.TextVerifiedManualRenewalResp{} + statuCode, err := client.Post(url, nil, headers, &mapData) + + if err != nil { + e.Log.Errorf("修改长效续期状态失败 %v", err) + } + + if statuCode >= 200 && statuCode < 300 { + return statuscode.Success + } else { + e.Log.Errorf("修改长效续期状态失败 %d", statuCode) + // 没有需要续费的号码 重新设置可自动续费之后再试一次 + if statuCode == 400 && strings.Contains(err.Error(), "Nothing to renew") { + return statuscode.NothingToRenew + } + + return statuscode.ServerError + } +} + // 重新初始化短信记录 func (e *SmsTextVerified) InitSmsLogs() error { // 获取平台密钥信息 @@ -1124,3 +1168,70 @@ func getIdByUrl(URLString string) string { func isValidID(s string) bool { return idPattern.MatchString(s) } + +// 获取长效租赁号码列表 +// next 分页url +func (e *SmsTextVerified) GetRentalList(next string, apiInfo *dto.SmsPlatformKeyQueueDto) (dto.TextVerifiedGetRentalListResp, error) { + var result dto.TextVerifiedGetRentalListResp + + client, code := e.GetTextVerifiedAuthClient(apiInfo) + if code != statuscode.Success { + e.Log.Errorf("获取长效号码列表失败 %d", code) + return result, fmt.Errorf("获取长效号码列表失败 %d", code) + } + + if next == "" { + next = rentalList + } else { + client.BaseURL = "" + } + + resp := dto.TextVerifiedGetRentalListResp{} + statusCode, err := client.Get(next, map[string]string{}, &resp) + if err != nil { + e.Log.Errorf("获取长效号码列表失败 %v", err) + return result, fmt.Errorf("获取长效号码列表失败 %v", err) + } + + if statusCode == http.StatusOK { + result = resp + return result, nil + } else if statusCode == http.StatusUnauthorized { + return e.GetRentalList(next, apiInfo) + } else { + e.Log.Errorf("获取长效号码列表失败 %d", statusCode) + return result, fmt.Errorf("获取长效号码列表失败 %d", statusCode) + } +} + +// 获取续费周期详情 +func (e *SmsTextVerified) GetBillingCycle(billingCycleId string, apiInfo *dto.SmsPlatformKeyQueueDto) (dto.TextVerifiedGetBillingCycleResp, error) { + var result dto.TextVerifiedGetBillingCycleResp + + client, code := e.GetTextVerifiedAuthClient(apiInfo) + if code != statuscode.Success { + e.Log.Errorf("获取计费周期详情失败 %d", code) + return result, fmt.Errorf("获取计费周期详情失败 %d", code) + } + + next := fmt.Sprintf(getBillingCycles, billingCycleId) + + resp := dto.TextVerifiedGetBillingCycleResp{} + statusCode, err := client.Get(next, map[string]string{}, &resp) + if err != nil { + e.Log.Errorf("获取计费周期详情失败 %v", err) + return result, fmt.Errorf("获取计费周期详情失败 %v", err) + } + + if statusCode >= http.StatusOK && statusCode < http.StatusMultipleChoices { + result = resp + return result, nil + } else if statusCode == http.StatusUnauthorized { + return e.GetBillingCycle(billingCycleId, apiInfo) + } else if statusCode == http.StatusNotFound { + return result, LogNotFund + } else { + e.Log.Errorf("获取计费周期详情失败 %d", statusCode) + return result, fmt.Errorf("获取计费周期详情失败 %d", statusCode) + } +} diff --git a/app/jobs/proxy_job.go b/app/jobs/proxy_job.go index d7819b5..e6f6ee1 100644 --- a/app/jobs/proxy_job.go +++ b/app/jobs/proxy_job.go @@ -37,7 +37,7 @@ func (j RenewalJob) Exec(args interface{}) error { // 定时短信续期任务 func (j SmsRenewalJob) Exec(args interface{}) error { - smsPhoneService := service.SmsPhone{} + smsPhoneService := service.SmsRenewalLog{} smsPhoneService.Orm = GetDb() smsPhoneService.Log = logger.NewHelper(logger.DefaultLogger) diff --git a/app/jobs/trx_job_test.go b/app/jobs/trx_job_test.go index a7f4de4..d7c4355 100644 --- a/app/jobs/trx_job_test.go +++ b/app/jobs/trx_job_test.go @@ -23,7 +23,7 @@ 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" diff --git a/common/statuscode/status_code.go b/common/statuscode/status_code.go index 9ba9dec..a23a60f 100644 --- a/common/statuscode/status_code.go +++ b/common/statuscode/status_code.go @@ -42,6 +42,7 @@ var StatusCodeZh = map[int]string{ SmsOutOfStockOrUnavailable: "短信验证码_缺货或服务不可用", SmsRentalRefundNotPermitted: "短信验证码_租赁退款不允许", SmsRentalCantRenew: "短信验证码_无法续期", + SmsRenewalLogExisted: "短信验证码_重复续期", } var StatusCodeEn = map[int]string{ @@ -76,6 +77,7 @@ var StatusCodeEn = map[int]string{ 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 获取状态码对应的消息 @@ -109,7 +111,8 @@ const ( AccountOrPasswordError = 10003 //密码不一致 PassWordNotMatch = 10004 - + // 没有需要续费的号码 重新设置可自动续费之后再试一次 + NothingToRenew = 11005 // ServerError = 500 @@ -165,4 +168,6 @@ const ( SmsRentalRefundNotPermitted = 20025 // 短信-无法续期 SmsRentalCantRenew = 20026 + // 短信-重复续期 + SmsRenewalLogExisted = 20028 )