diff --git a/app/admin/models/sms_phone.go b/app/admin/models/sms_phone.go index 9d13c94..7115e06 100644 --- a/app/admin/models/sms_phone.go +++ b/app/admin/models/sms_phone.go @@ -23,9 +23,13 @@ type SmsPhone struct { 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:备注"` + PlatformName string `json:"platformName" gorm:"-"` models.ModelTime models.ControlBy } diff --git a/app/admin/models/sms_receive_log.go b/app/admin/models/sms_receive_log.go index 88f3c8d..b5e6ddd 100644 --- a/app/admin/models/sms_receive_log.go +++ b/app/admin/models/sms_receive_log.go @@ -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-失败"` diff --git a/app/admin/service/dto/sms_phone.go b/app/admin/service/dto/sms_phone.go index 98040e3..f87d724 100644 --- a/app/admin/service/dto/sms_phone.go +++ b/app/admin/service/dto/sms_phone.go @@ -150,8 +150,10 @@ type WeakUpReq struct { } type WeakUpResp struct { - ActivationId string `json:"activationId" comment:"激活id"` - MessageId string `json:"messageId" commment:"消息id"` + 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 { @@ -190,9 +192,12 @@ type SmsPhoneCleanMyPhoneReq struct { } type SmsPhoneGetPhoneResp struct { - Phone string `json:"phone"` - EndAt *time.Time `json:"endTime"` - 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"` } type DaisysmsPriceResp struct { @@ -208,6 +213,8 @@ type OpenGetNumberResp struct { 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 { diff --git a/app/admin/service/dto/text_verified.go b/app/admin/service/dto/text_verified.go index fcff52d..c57f8bf 100644 --- a/app/admin/service/dto/text_verified.go +++ b/app/admin/service/dto/text_verified.go @@ -212,3 +212,13 @@ type TextVerifiedSmsItemResp struct { 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 TextVerifiedGetNumbersLinksResp struct { + Next *TextVerifiedResp `json:"next"` +} diff --git a/app/admin/service/member_recharge.go b/app/admin/service/member_recharge.go index 60eb792..614ad62 100644 --- a/app/admin/service/member_recharge.go +++ b/app/admin/service/member_recharge.go @@ -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 { diff --git a/app/admin/service/sms_phone.go b/app/admin/service/sms_phone.go index 4fd89bf..98885d0 100644 --- a/app/admin/service/sms_phone.go +++ b/app/admin/service/sms_phone.go @@ -36,6 +36,8 @@ func (e SmsPhone) OpenGetNumber(req *dto.GetNumberReq, userId int) (dto.OpenGetN resp.ActivationId = smsPhone.NewActivationId resp.ExpireTime = smsPhone.ExpireTime resp.Number = smsPhone.Phone + resp.StartTime = smsPhone.StartTime + resp.EndTime = smsPhone.EndTime return resp, statuscode.Success } @@ -64,7 +66,7 @@ func (e *SmsPhone) CancelNumber(req *dto.SmsPhoneCancelNumberReq, userId int) in if code == statuscode.Success { err := e.Orm.Transaction(func(tx *gorm.DB) error { - if err1 := e.Orm.Model(data).Updates(map[string]interface{}{"status": 3, "code": "", "actived": "3", "auto_renewal": 2}).Error; err1 != nil { + if err1 := e.Orm.Model(data).Updates(map[string]interface{}{"status": 3, "code": "", "actived": "3", "auto_renewal": 2, "deleted_at": time.Now()}).Error; err1 != nil { e.Log.Errorf("更新短信号码失败, %s", err1) return err1 } @@ -143,21 +145,27 @@ func (e *SmsPhone) WeakUp(req *dto.WeakUpReq, userId int, defult bool) (dto.Weak } if smsPhone.Status != 1 || defult { - newActivationId, messageId, code := e.WeakUpManage(&smsPhone) + newActivationId, messageId, startTime, endTime, code := e.WeakUpManage(&smsPhone) if code == statuscode.ServerError { return result, code } else if code == statuscode.Success { - if err := e.Orm.Model(smsPhone).Updates(map[string]interface{}{"new_activation_id": newActivationId, "status": 1, "code": "", "message_id": messageId}).Error; err != nil { + if err := e.Orm.Model(smsPhone). + Updates(map[string]interface{}{ + "new_activation_id": newActivationId, "status": 1, "code": "", + "message_id": messageId, "start_time": startTime, "end_time": endTime}).Error; err != nil { e.Log.Errorf("更新短信号码失败, %s", err) return result, statuscode.ServerError } result.ActivationId = newActivationId result.MessageId = messageId + result.StartTime = startTime + result.EndTime = endTime } else if code == statuscode.SmsLongNumWaitCode { - e.Log.Info("无须唤醒") - if err := e.Orm.Model(smsPhone).Updates(map[string]interface{}{"status": 1, "code": ""}).Error; err != nil { + if err := e.Orm.Model(smsPhone). + Updates(map[string]interface{}{"status": 1, "code": "", "start_time": startTime, "end_time": endTime}). + Error; err != nil { e.Log.Errorf("更新短信号码失败, %s", err) return result, statuscode.ServerError } @@ -171,18 +179,18 @@ func (e *SmsPhone) WeakUp(req *dto.WeakUpReq, userId int, defult bool) (dto.Weak // 唤醒号码 // returns newActivationId,messageId, code -func (e *SmsPhone) WeakUpManage(req *models.SmsPhone) (string, string, int) { +func (e *SmsPhone) WeakUpManage(req *models.SmsPhone) (string, string, *time.Time, *time.Time, int) { switch req.PlatformCode { case global.SmsPlatformDaisysms: service := SmsDaisysms{Service: e.Service} id, code := service.getExtraActivation(req.ActivationId) - return strconv.Itoa(id), "", code + return strconv.Itoa(id), "", nil, nil, code case global.SmsPlatformTextVerified: service := SmsTextVerified{Service: e.Service} - - return service.getExtraActivation(req.NewActivationId) + resp, code := service.getExtraActivation(req.NewActivationId) + return resp.ReservationId, resp.Id, resp.UsageWindowStart, resp.UsageWindowEnd, code default: - return "", "", statuscode.SmsPlatformUnavailable + return "", "", nil, nil, statuscode.SmsPlatformUnavailable } } @@ -201,6 +209,17 @@ func (e *SmsPhone) GetMyPage(req *dto.SmsPhoneGetPageReq, userId int, phone *[]m return statuscode.ServerError } + dictService := SysDictData{} + dictService.Orm = e.Orm + dictService.Log = e.Log + dictDatas, _ := dictService.GetMapByType("sms_platform") + + for index := range *phone { + if dict, ok := dictDatas[(*phone)[index].PlatformCode]; ok { + (*phone)[index].PlatformName = dict.DictLabel + } + } + return statuscode.Success } @@ -281,7 +300,7 @@ func (e *SmsPhone) DoGetNumber(balanceService *MemberBalance, req *dto.GetNumber } now := time.Now() - activationId, phone, code, expireTime := e.GetNumberManage(req.PlatformCode, req.Type, req.ServiceCode, price, req.Period) + activationId, phone, code, expireTime, startTime, endTime := e.GetNumberManage(req.PlatformCode, req.Type, req.ServiceCode, price, req.Period) if code != statuscode.Success { return decimal.Decimal{}, models.SmsPhone{}, code @@ -299,6 +318,8 @@ func (e *SmsPhone) DoGetNumber(balanceService *MemberBalance, req *dto.GetNumber smsPhone.MessageId = activationId smsPhone.NewActivationId = activationId smsPhone.Actived = 1 + smsPhone.StartTime = startTime + smsPhone.EndTime = endTime smsPhone.Price = price if req.Type == 1 { @@ -310,6 +331,10 @@ func (e *SmsPhone) DoGetNumber(balanceService *MemberBalance, req *dto.GetNumber smsPhone.ExpireTime = &now } + //平台不允许取消 + if req.PlatformCode == global.SmsPlatformTextVerified { + smsPhone.Actived = 2 + } } else { if expireTime != nil { smsPhone.ExpireTime = expireTime @@ -355,24 +380,30 @@ func (e *SmsPhone) DoGetNumber(balanceService *MemberBalance, req *dto.GetNumber } // 租赁号码 -func (e *SmsPhone) GetNumberManage(platformCode string, typ int, serviceCode string, price decimal.Decimal, period int) (string, string, int, *time.Time) { +// return activationId 激活id +// phone 号码 +// code 状态码 +// *expirateTime 号码过期时间 +// *startTime 长效号码单次接码开始使用(textverified有用) +// *endTime 长效号码单次接码过期时间(textverified有用) +func (e *SmsPhone) GetNumberManage(platformCode string, typ int, serviceCode string, price decimal.Decimal, period int) (string, string, int, *time.Time, *time.Time, *time.Time) { switch platformCode { case global.SmsPlatformDaisysms: service := SmsDaisysms{Service: e.Service} activationId, phone, code := service.GetNumberForApi(typ, serviceCode, price, period) - return strconv.Itoa(activationId), phone, code, nil + return strconv.Itoa(activationId), phone, code, nil, nil, nil case global.SmsPlatformTextVerified: service := SmsTextVerified{Service: e.Service} - resp, code := service.GetNumberForApi(typ, serviceCode, price, period) + resp, code := service.GetNumberAndWakeUp(typ, serviceCode, price, period) if code != statuscode.Success { - return "", "", code, nil + return "", "", code, nil, resp.StartTime, resp.EndTime } - return resp.Id, resp.Phone, code, resp.EndAt + return resp.Id, resp.Phone, code, resp.EndAt, resp.StartTime, resp.EndTime default: - return "", "", statuscode.SmsPlatformUnavailable, nil + return "", "", statuscode.SmsPlatformUnavailable, nil, nil, nil } } @@ -416,11 +447,17 @@ func (e *SmsPhone) SyncCodes() error { } for _, item := range phones { - code, codeStatus := e.GetCodeManage(item.PlatformCode, item.NewActivationId, item.Type, item.UpdatedAt.Unix()) + code, codeStatus := e.GetCodeManage(item.PlatformCode, item.NewActivationId, + item.Type, item.UserId, item.Service, item.ServiceCode) if code == "" { - expireTime := item.UpdatedAt.Add(time.Second * 150) - if item.PlatformCode == global.SmsPlatformTextVerified && expireTime.Before(time.Now()) { + var expireTime time.Time + + if item.EndTime != nil { + expireTime = *item.EndTime + } + + if (item.Status == 3 && item.Actived == 2) || (item.PlatformCode == global.SmsPlatformTextVerified && expireTime.Before(time.Now())) { if err := e.Orm.Model(&item).Updates(map[string]interface{}{"status": 3, "updated_at": time.Now()}).Error; err != nil { e.Log.Errorf("手机号一睡眠 %s", item.Phone) } @@ -473,7 +510,7 @@ func (e *SmsPhone) SyncCodes() error { } // 获取验证码 -func (e *SmsPhone) GetCodeManage(platformCode string, messageId string, typ int, unixTime int64) (string, int) { +func (e *SmsPhone) GetCodeManage(platformCode string, messageId string, typ int, userId int, smsService, serviceCode string) (string, int) { switch platformCode { case global.SmsPlatformDaisysms: service := SmsDaisysms{Service: e.Service} @@ -481,98 +518,224 @@ func (e *SmsPhone) GetCodeManage(platformCode string, messageId string, typ int, return service.GetCodeForApi(messageId) case global.SmsPlatformTextVerified: service := SmsTextVerified{Service: e.Service} - return service.GetCode(messageId, typ, unixTime) + return service.GetCode(messageId, typ, userId, smsService, serviceCode) default: return "", statuscode.SmsPlatformUnavailable } } -// 自动续期 -func (e SmsServices) AutoRenewal() error { - var datas []models.SmsPhone - var data models.SmsPhone - startTime := time.Now().Add(-24 * time.Hour) - endTime := time.Now().Add(24 * time.Hour) - - smsServices := SmsServices{Service: e.Service} - serviceMap := smsServices.GetMapAll() - - configService := SysConfig{Service: e.Service} - mapDatas, _ := configService.GetMapByKeys([]string{"renew_number_premium_daisysms", "renew_number_premium_textverified"}) - - if err := e.Orm.Model(&models.SmsPhone{}).Where("auto_renewal =1 and type =1 and actived =2 and expire_time >=? and expire_time 0 { - per := decimal.NewFromInt(100).Add(val) - percent = per.Div(decimal.NewFromInt(100)).Truncate(2) - } - case item.PlatformCode == global.SmsPlatformTextVerified: - p, ok := mapDatas["renew_number_premium_textverified"] - val, _ := decimal.NewFromString(p.ConfigValue) - - if ok && val.Cmp(decimal.Zero) > 0 { - per := decimal.NewFromInt(100).Add(val) - percent = per.Div(decimal.NewFromInt(100)).Truncate(2) - } - } - - renewLog := models.SmsRenewalLog{} - renewLog.UserId = item.UserId - renewLog.PhoneId = item.Id - renewLog.Type = item.Type - renewLog.Amount = price.Mul(percent).Truncate(2) - renewLog.BeforeTime = *item.ExpireTime - renewLog.Period = 30 - - if err := e.Orm.Transaction(func(tx *gorm.DB) error { - db := tx.Exec("UPDATE member_balance set balance = balance - ? where user_id = ? and balance >= ?", price, item.UserId, price) - - if db.Error != nil { - return db.Error - } - - if db.RowsAffected == 0 { - return errors.New("余额不足") - } - - if err1 := tx.Create(&renewLog).Error; err1 != nil { - - return errors.New("创建续费日志失败") - } - - newExpireTime := item.ExpireTime.AddDate(0, 0, 30) - if err1 := tx.Model(data).Where("id =?", item.Id).Update("expire_time", newExpireTime).Error; err1 != nil { - return err1 - } - return nil - }); err != nil { - e.Log.Errorf("自动续期失败:%s", err.Error()) - } } } return 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, + 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.Error() == "余额不足" { + + 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.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 + if req.AutoRenew == 1 { + smsServices := SmsServices{Service: e.Service} + balanceService := MemberBalance{Service: e.Service} + balance := balanceService.GetBalance(userId) + + if err := e.Orm.Model(data).Where("id =? or activation_id =?", req.Id, req.ActivationId).First(&data).Error; err != nil { + e.Log.Errorf("修改自动续期 数据不存在 id:%d err:%s", req.Id, err.Error()) + return statuscode.SmsNotExisted + } + + if data.ExpireTime.Before(time.Now()) { + return statuscode.SmsRentalCantRenew + } + + serviceItem, err := smsServices.GetByCode(data.PlatformCode, data.ServiceCode) + + if err != nil { + e.Log.Errorf("短信服务报错:%v", err) + return statuscode.ServerError + } + configService := SysConfig{Service: e.Service} + configKeys := []string{"renew_number_premium_daisysms", "renew_number_premium_textverified"} + premiumMap, _ := configService.GetMapByKeys(configKeys) + + price := e.calculateRenewalPrice(serviceItem.LongPrice, serviceItem.PlatformCode, premiumMap) + + if balance.Cmp(price) < 0 { + return statuscode.BalanceNotEnough + } + } + if req.Id > 0 { if err := e.Orm.Model(data).Where("id =? and user_id =?", req.Id, userId).First(&data).Error; err != nil { e.Log.Errorf("修改自动续期 数据不存在 id:%d err:%s", req.Id, err.Error()) diff --git a/app/admin/service/sms_receive_log.go b/app/admin/service/sms_receive_log.go index f9b7d6c..7989184 100644 --- a/app/admin/service/sms_receive_log.go +++ b/app/admin/service/sms_receive_log.go @@ -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, diff --git a/app/admin/service/sms_receive_log_test.go b/app/admin/service/sms_receive_log_test.go index 30b244f..a8f22a7 100644 --- a/app/admin/service/sms_receive_log_test.go +++ b/app/admin/service/sms_receive_log_test.go @@ -13,7 +13,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.TrxGridUrl = "https://api.trongrid.io" diff --git a/app/admin/service/sms_text_verified.go b/app/admin/service/sms_text_verified.go index 22c6b63..d1989e8 100644 --- a/app/admin/service/sms_text_verified.go +++ b/app/admin/service/sms_text_verified.go @@ -32,7 +32,7 @@ type SmsTextVerified struct { // 获取收到的验证码 // messageId 短效或长效号码的ID // typ 0-短效 1-长效 -func (e SmsTextVerified) GetCode(messageId string, typ int, unixTime int64) (string, int) { +func (e SmsTextVerified) GetCode(messageId string, typ int, userId int, service, serviceCode string) (string, int) { reservationType := "" if typ == 0 { @@ -58,23 +58,84 @@ func (e SmsTextVerified) GetCode(messageId string, typ int, unixTime int64) (str return "", statuscode.ServerError } + messageIds := []string{} + if len(resp.Data) > 0 { for _, v := range resp.Data { - // 2. 解析时间字符串 - // time.RFC3339Nano 是一个预定义的格式常量, - // 它能够解析包含纳秒和时区信息的 ISO 8601 字符串。 + messageIds = append(messageIds, v.Id) + } + } + + if len(messageIds) > 0 { + receiveLogs := make([]models.SmsReceiveLog, 0) + receiveMaps := make(map[string]string, 0) + + if err := e.Orm.Model(&models.SmsReceiveLog{}). + Where("message_id in ?", messageIds). + Select("message_id"). + Find(&receiveLogs).Error; err != nil { + e.Log.Errorf("获取短信接收记录失败, error: %v", err) + return "", statuscode.ServerError + } + + for _, v := range receiveLogs { + receiveMaps[v.MessageId] = v.MessageId + } + + // 查找最新验证码并存储所有新验证码 + var latestSms *dto.TextVerifiedSmsItemResp + var latestTime time.Time + var newSmsData []dto.TextVerifiedSmsItemResp + + // 遍历所有验证码数据 + for _, v := range resp.Data { + // 解析时间字符串 t, err := time.Parse(time.RFC3339Nano, v.CreatedAt) if err != nil { - fmt.Println("时间解析失败:", err) + e.Log.Errorf("时间解析失败: %v", err) + continue } - // 3. 将解析后的 time.Time 对象转换为 Unix 时间戳 - // t.Unix() 返回以秒为单位的整数时间戳 - unixTimestamp := t.Unix() + // 检查是否为新验证码(数据库中不存在) + if _, ok := receiveMaps[v.Id]; !ok { + newSmsData = append(newSmsData, v) + } - if unixTimestamp >= unixTime { - parsedCode = v.ParsedCode - break + // 选择最新的验证码(包括已存在的) + if latestSms == nil || t.After(latestTime) { + latestSms = &v + latestTime = t + } + } + + // 批量保存所有新验证码到数据库 + if len(newSmsData) > 0 { + var receiveLogs []models.SmsReceiveLog + for _, sms := range newSmsData { + receiveLog := models.SmsReceiveLog{ + UserId: userId, + Service: service, + ServiceCode: serviceCode, + MessageId: sms.Id, + Phone: "1" + sms.To, + Code: sms.ParsedCode, + Status: 2, + } + receiveLogs = append(receiveLogs, receiveLog) + } + + // 批量插入数据库 + if err := e.Orm.Create(&receiveLogs).Error; err != nil { + e.Log.Errorf("批量创建短信接收记录失败, error: %v", err) + return "", statuscode.ServerError + } + + e.Log.Infof("成功存储 %d 条新验证码记录", len(newSmsData)) + + // 返回最新的验证码 + if latestSms != nil { + parsedCode = latestSms.ParsedCode + e.Log.Infof("返回最新验证码: %s, messageId: %s, 时间: %s", parsedCode, latestSms.Id, latestSms.CreatedAt) } } } @@ -97,6 +158,7 @@ const ( 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}$`) @@ -284,12 +346,33 @@ func (e *SmsTextVerified) SyncPrices() error { return nil } +// 获取号码并唤醒 +func (e *SmsTextVerified) GetNumberAndWakeUp(tye int, serviceCode string, price decimal.Decimal, period int) (dto.SmsPhoneGetPhoneResp, int) { + resp, code := e.GetNumberForApi(tye, serviceCode, price, period) + + if code != statuscode.Success { + return resp, code + } + + wakeUpResp, code := e.getExtraActivation(resp.Id) + + if code != statuscode.Success { + return resp, code + } + + resp.MessageId = wakeUpResp.Id + resp.StartTime = wakeUpResp.UsageWindowStart + resp.EndTime = wakeUpResp.UsageWindowEnd + + return resp, code +} + // 号码租赁 // getType 0-短效 1-长效 // service 服务code // maxPrice 最大价格 // period 时长(月=30天) -func (e SmsTextVerified) GetNumberForApi(typ int, serviceCode string, price decimal.Decimal, period int) (dto.SmsPhoneGetPhoneResp, int) { +func (e *SmsTextVerified) GetNumberForApi(typ int, serviceCode string, price decimal.Decimal, period int) (dto.SmsPhoneGetPhoneResp, int) { //这个平台的所有号码都是美国号码 默认国家代码都是 +1 var err error var createResp dto.TextVerifiedResp @@ -477,12 +560,13 @@ func (e *SmsTextVerified) GetRentalDetail(id string) (dto.VerificationRentalDeta } // 唤醒号码 -// returns activationId,messageId, code -func (e SmsTextVerified) getExtraActivation(id string) (string, string, int) { +// returns activationId,messageId,startTime(可用开始时间),endTime(可用结束时间), code, +func (e SmsTextVerified) getExtraActivation(id string) (dto.TextVerifiedWakeUpResp, int) { client, code := e.GetTextVerifiedAuthClient() + result := dto.TextVerifiedWakeUpResp{} if code != statuscode.Success { - return "", "", code + return result, code } req := dto.TextVerifiedWakeUpReq{ @@ -497,31 +581,35 @@ func (e SmsTextVerified) getExtraActivation(id string) (string, string, int) { if err != nil { e.Log.Errorf("唤醒号码失败 id:%s error: %v", id, err) - return "", "", statuscode.ServerError + return result, statuscode.ServerError } else if status == http.StatusCreated || status == http.StatusOK { if resp.Method != "" && resp.Href != "" { bytes, err := e.doRequest(&resp) if err != nil { e.Log.Errorf("唤醒号码失败 id:%s error: %v", id, err) - return "", "", statuscode.ServerError + return result, statuscode.ServerError } detailResp := dto.TextVerifiedWakeUpResp{} if err := sonic.Unmarshal(bytes, &detailResp); err != nil { e.Log.Errorf("唤醒号码反序列化失败 id:%s error: %v", id, err) - return "", "", statuscode.ServerError + return result, statuscode.ServerError } - return detailResp.ReservationId, detailResp.Id, statuscode.Success + result.Id = detailResp.Id + result.ReservationId = detailResp.ReservationId + result.UsageWindowEnd = detailResp.UsageWindowEnd + result.UsageWindowStart = detailResp.UsageWindowStart + return result, statuscode.Success } else { e.Log.Errorf("唤醒号码失败 id:%s error: %v", id, err) - return "", "", statuscode.ServerError + return result, statuscode.ServerError } } - return "", "", statuscode.ServerError + return result, statuscode.ServerError } // 获取价格 @@ -826,7 +914,121 @@ func (e *SmsTextVerified) Renew(activationId string, status bool) int { } } +// 重新初始化短信记录 +func (e *SmsTextVerified) InitSmsLogs() error { + // phones, err := e.GetNumbers() + + // if err != nil { + // e.Log.Errorf("获取长效号码列表失败 %v", err) + // return fmt.Errorf("获取长效号码列表失败 %v", err) + // } + + // numbers := make([]string, 0) + + // for _, v := range phones { + // numbers = append(numbers, v.Number) + // } + + phoneLogs, err := e.GetUserByNumber("textverified") + + if err != nil { + e.Log.Errorf("获取系统内号码记录失败 %v", err) + return fmt.Errorf("获取系统内号码记录失败 %v", err) + } + + if len(phoneLogs) > 0 { + for _, v := range phoneLogs { + e.GetCode(v.ActivationId, v.Type, v.UserId, v.Service, v.ServiceCode) + } + } + + return nil +} + +// 根据电话号码获取系统内记录 +func (e *SmsTextVerified) GetUserByNumber(platformCode string) ([]models.SmsPhone, error) { + var result []models.SmsPhone + + if err := e.Orm.Model(&models.SmsPhone{}).Where("platform_code = ?", platformCode).Find(&result).Error; err != nil { + return nil, err + } + + return result, nil +} + +// 获取号码 +func (e *SmsTextVerified) GetNumbers() ([]dto.VerificationDTO, error) { + client, code := e.GetTextVerifiedAuthClient() + result := make([]dto.VerificationDTO, 0) + + if code != statuscode.Success { + e.Log.Errorf("获取长效号码列表失败 %d", code) + return result, fmt.Errorf("获取长效号码列表失败 %d", code) + } + + resp := dto.TextVerifiedGetNumbersResp{} + statusCode, err := client.Get(getNumbers, map[string]string{}, resp) + + if err != nil { + e.Log.Errorf("获取长效号码列表失败 %v", err) + return result, fmt.Errorf("获取长效号码列表失败 %v", err) + } + + if statusCode == http.StatusOK { + // 添加第一页数据 + result = append(result, resp.Data...) + + // 处理分页数据 + if resp.HasNext { + paginatedData, err := e.fetchPaginatedData(resp.Links.Next) + if err != nil { + return result, err + } + result = append(result, paginatedData...) + } + + return result, nil + } else if statusCode == http.StatusUnauthorized { + return e.GetNumbers() + } else { + e.Log.Errorf("获取长效号码列表失败 %d", statusCode) + return result, fmt.Errorf("获取长效号码列表失败 %d", statusCode) + } +} + // 获取授权header头 +// fetchPaginatedData 获取分页数据的通用方法 +// 处理TextVerified API的分页响应,自动获取所有页面的数据 +func (e *SmsTextVerified) fetchPaginatedData(nextLink *dto.TextVerifiedResp) ([]dto.VerificationDTO, error) { + var result []dto.VerificationDTO + + for nextLink != nil { + nextData, err := e.doRequest(nextLink) + if err != nil { + e.Log.Errorf("获取分页数据失败: %v", err) + return result, fmt.Errorf("获取分页数据失败: %v", err) + } + + nextResp := dto.TextVerifiedGetNumbersResp{} + err = sonic.Unmarshal(nextData, &nextResp) + if err != nil { + e.Log.Errorf("解析分页数据失败: %v", err) + return result, fmt.Errorf("解析分页数据失败: %v", err) + } + + result = append(result, nextResp.Data...) + + // 检查是否还有下一页 + if nextResp.HasNext { + nextLink = nextResp.Links.Next + } else { + nextLink = nil + } + } + + return result, nil +} + func (e *SmsTextVerified) GetAuthHeader() (map[string]string, error) { token, err := e.GetToken() if err != nil { diff --git a/app/admin/service/sms_text_verified_test.go b/app/admin/service/sms_text_verified_test.go index 0f1f111..9caf354 100644 --- a/app/admin/service/sms_text_verified_test.go +++ b/app/admin/service/sms_text_verified_test.go @@ -51,3 +51,18 @@ func TestSyncTextVerifiedServices(t *testing.T) { 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") + } +} diff --git a/app/jobs/proxy_job.go b/app/jobs/proxy_job.go index ad16381..d7819b5 100644 --- a/app/jobs/proxy_job.go +++ b/app/jobs/proxy_job.go @@ -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.SmsPhone{} + smsPhoneService.Orm = GetDb() + smsPhoneService.Log = logger.NewHelper(logger.DefaultLogger) - return smsService.AutoRenewal() + return smsPhoneService.AutoRenewal() } // 过期任务 diff --git a/common/statuscode/status_code.go b/common/statuscode/status_code.go index 9e50797..9ba9dec 100644 --- a/common/statuscode/status_code.go +++ b/common/statuscode/status_code.go @@ -41,6 +41,7 @@ var StatusCodeZh = map[int]string{ SmsInvalidType: "短信验证码_无效类型", SmsOutOfStockOrUnavailable: "短信验证码_缺货或服务不可用", SmsRentalRefundNotPermitted: "短信验证码_租赁退款不允许", + SmsRentalCantRenew: "短信验证码_无法续期", } var StatusCodeEn = map[int]string{ @@ -74,6 +75,7 @@ var StatusCodeEn = map[int]string{ SmsInvalidType: "sms type invalid", SmsOutOfStockOrUnavailable: "sms out of stock or unavailable", SmsRentalRefundNotPermitted: "sms rental refund not permitted", + SmsRentalCantRenew: "sms rental can not renewal", } // GetMsg 获取状态码对应的消息 @@ -161,4 +163,6 @@ const ( SmsOutOfStockOrUnavailable = 20024 // 短信-租赁退款不允许 SmsRentalRefundNotPermitted = 20025 + // 短信-无法续期 + SmsRentalCantRenew = 20026 )