diff --git a/backend_v1/app/http/admin/routes.gen.go b/backend_v1/app/http/admin/routes.gen.go index 6c7862e..00217bc 100644 --- a/backend_v1/app/http/admin/routes.gen.go +++ b/backend_v1/app/http/admin/routes.gen.go @@ -156,6 +156,11 @@ func (r *Routes) Register(router fiber.Router) { r.smsCodeSends.List, Query[dto.SmsCodeSendListQuery]("query"), )) + r.log.Debugf("Registering route: Post /admin/v1/sms-code-sends/manual-set -> smsCodeSends.ManualSet") + router.Post("/admin/v1/sms-code-sends/manual-set"[len(r.Path()):], DataFunc1( + r.smsCodeSends.ManualSet, + Body[SmsCodeManualSetBody]("body"), + )) // Register routes for controller: statistics r.log.Debugf("Registering route: Get /admin/v1/statistics -> statistics.statistics") router.Get("/admin/v1/statistics"[len(r.Path()):], DataFunc0( diff --git a/backend_v1/app/http/admin/sms_code_sends.go b/backend_v1/app/http/admin/sms_code_sends.go index cf6edfa..b3b1f47 100644 --- a/backend_v1/app/http/admin/sms_code_sends.go +++ b/backend_v1/app/http/admin/sms_code_sends.go @@ -6,6 +6,7 @@ import ( "quyun/v2/app/services" "quyun/v2/database" "quyun/v2/database/models" + "time" "github.com/gofiber/fiber/v3" ) @@ -13,6 +14,11 @@ import ( // @provider type smsCodeSends struct{} +type SmsCodeManualSetBody struct { + Phone string `json:"phone"` + Code string `json:"code"` +} + // List // // @Summary 短信验证码发送记录 @@ -58,3 +64,17 @@ func (ctl *smsCodeSends) List(ctx fiber.Ctx, query *dto.SmsCodeSendListQuery) (* Items: items, }, nil } + +// ManualSet +// +// @Summary 手动设置短信验证码(用于短信认证) +// @Tags Admin SMS +// @Accept json +// @Produce json +// @Param body body SmsCodeManualSetBody true "请求体" +// @Success 200 {object} models.SmsCodeSend "成功" +// @Router /admin/v1/sms-code-sends/manual-set [post] +// @Bind body body +func (ctl *smsCodeSends) ManualSet(ctx fiber.Ctx, body *SmsCodeManualSetBody) (*models.SmsCodeSend, error) { + return services.Users.SetPhoneCode(ctx.Context(), body.Phone, body.Code, 5*time.Minute) +} diff --git a/backend_v1/app/services/users.go b/backend_v1/app/services/users.go index 2e77d58..715e532 100644 --- a/backend_v1/app/services/users.go +++ b/backend_v1/app/services/users.go @@ -359,6 +359,64 @@ func (m *users) gen4Digits() (string, error) { return fmt.Sprintf("%04d", n.Int64()), nil } +// SetPhoneCode 手动设置短信验证码(后台操作);默认有效期 5 分钟,不受发送频率限制。 +func (m *users) SetPhoneCode(ctx context.Context, phone, code string, ttl time.Duration) (*models.SmsCodeSend, error) { + phone = m.normalizePhone(phone) + code = strings.TrimSpace(code) + if phone == "" { + return nil, errors.New("手机号不能为空") + } + if code == "" { + return nil, errors.New("验证码不能为空") + } + if len(code) != 4 { + return nil, errors.New("验证码必须为 4 位数字") + } + for _, r := range code { + if r < '0' || r > '9' { + return nil, errors.New("验证码必须为 4 位数字") + } + } + + // 前置校验:手机号必须已注册 + _, err := m.FindByPhone(ctx, phone) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("手机号未注册,请联系管理员开通") + } + return nil, errors.Wrap(err, "failed to find user by phone") + } + + now := time.Now() + if ttl <= 0 { + ttl = 5 * time.Minute + } + expiresAt := now.Add(ttl) + + m.mu.Lock() + m.ensurePhoneAuthMaps() + m.codeByPhone[phone] = phoneCodeEntry{code: code, expiresAt: expiresAt} + m.lastSentAtByPhone[phone] = now + m.mu.Unlock() + + if _db == nil { + return nil, errors.New("db not initialized") + } + + record := &models.SmsCodeSend{ + Phone: phone, + Code: code, + SentAt: now, + ExpiresAt: expiresAt, + } + if err := _db.WithContext(ctx).Create(record).Error; err != nil { + return nil, err + } + + log.Infof("SetPhoneCode to %s: code=%s", phone, code) + return record, nil +} + // SendPhoneCode 发送短信验证码(内存限流:同一手机号 58s 内仅允许发送一次;验证码 5 分钟过期)。 func (m *users) SendPhoneCode(ctx context.Context, phone string) error { phone = m.normalizePhone(phone) diff --git a/backend_v1/database/models/sms_code_send.go b/backend_v1/database/models/sms_code_send.go deleted file mode 100644 index 98e1f87..0000000 --- a/backend_v1/database/models/sms_code_send.go +++ /dev/null @@ -1,16 +0,0 @@ -package models - -import "time" - -const TableNameSmsCodeSend = "sms_code_sends" - -// SmsCodeSend mapped from table -type SmsCodeSend struct { - ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` - Phone string `gorm:"column:phone;type:character varying(20);not null" json:"phone"` - Code string `gorm:"column:code;type:character varying(20);not null" json:"code"` - SentAt time.Time `gorm:"column:sent_at;type:timestamp without time zone;not null;default:now()" json:"sent_at"` - ExpiresAt time.Time `gorm:"column:expires_at;type:timestamp without time zone;not null" json:"expires_at"` -} - -func (*SmsCodeSend) TableName() string { return TableNameSmsCodeSend } diff --git a/frontend/admin/src/api/smsCodeSendService.js b/frontend/admin/src/api/smsCodeSendService.js index 92a4d85..dbadd7b 100644 --- a/frontend/admin/src/api/smsCodeSendService.js +++ b/frontend/admin/src/api/smsCodeSendService.js @@ -9,6 +9,11 @@ export const smsCodeSendService = { phone: phone.trim() } }); + }, + manualSet({ phone = '', code = '' } = {}) { + return httpClient.post('/sms-code-sends/manual-set', { + phone: phone.trim(), + code: code.trim() + }); } }; - diff --git a/frontend/admin/src/pages/SmsCodeSendPage.vue b/frontend/admin/src/pages/SmsCodeSendPage.vue index 5592e66..6fced36 100644 --- a/frontend/admin/src/pages/SmsCodeSendPage.vue +++ b/frontend/admin/src/pages/SmsCodeSendPage.vue @@ -1,8 +1,10 @@ -