44 "context"
55 "fmt"
66 "log/slog"
7+ "strings"
78 "time"
89
910 "github.com/google/uuid"
@@ -14,6 +15,7 @@ import (
1415 "github.com/chaitin/MonkeyCode/backend/db"
1516 "github.com/chaitin/MonkeyCode/backend/domain"
1617 "github.com/chaitin/MonkeyCode/backend/errcode"
18+ "github.com/chaitin/MonkeyCode/backend/pkg/crypto"
1719 "github.com/chaitin/MonkeyCode/backend/pkg/cvt"
1820)
1921
@@ -139,3 +141,113 @@ func (u *UserUsecase) GetUserByEmail(ctx context.Context, emails []string) ([]*d
139141 })
140142 return result , nil
141143}
144+
145+ // SendBindEmailVerification 发送邮箱绑定验证邮件
146+ func (u * UserUsecase ) SendBindEmailVerification (ctx context.Context , userID uuid.UUID , req * domain.SendBindEmailVerificationReq ) error {
147+ // 检查邮箱是否已被其他用户使用
148+ existingUsers , err := u .repo .GetUserByEmail (ctx , []string {req .Email })
149+ if err != nil && ! db .IsNotFound (err ) {
150+ return errcode .ErrDatabaseQuery .Wrap (err )
151+ }
152+ for _ , eu := range existingUsers {
153+ if eu .ID == userID {
154+ return errcode .ErrEmailAlreadyBound
155+ }
156+ return errcode .ErrEmailTaken
157+ }
158+
159+ // 生成验证 token
160+ token , err := crypto .Simple (userID .String (), time .Now ().Add (time .Hour * 24 ))
161+ if err != nil {
162+ u .logger .ErrorContext (ctx , "generate bind email token failed" , "error" , err )
163+ return errcode .ErrInternalServer .Wrap (err )
164+ }
165+
166+ // 存储 token 到 Redis,格式:{token}:{email},有效期 24 小时
167+ key := fmt .Sprintf ("bind_email_token:%s" , userID .String ())
168+ value := fmt .Sprintf ("%s:%s" , token , req .Email )
169+ if err := u .redis .Set (ctx , key , value , time .Hour * 24 ).Err (); err != nil {
170+ u .logger .ErrorContext (ctx , "set redis key failed" , "error" , err )
171+ return errcode .ErrDatabaseOperation .Wrap (err )
172+ }
173+
174+ // 获取用户信息用于邮件发送
175+ user , err := u .repo .Get (ctx , userID )
176+ if err != nil {
177+ u .logger .ErrorContext (ctx , "get user failed" , "error" , err )
178+ return errcode .ErrDatabaseQuery .Wrap (err )
179+ }
180+
181+ // 异步发送邮件
182+ verifyURL := fmt .Sprintf ("%s/api/v1/users/email/verify?token=%s" , u .config .Server .BaseURL , token )
183+ go func () {
184+ if err := u .email .SendBindEmailVerification (context .Background (), req .Email , user .Name , verifyURL ); err != nil {
185+ u .logger .ErrorContext (ctx , "send bind email verification mail failed" , "error" , err )
186+ }
187+ }()
188+
189+ return nil
190+ }
191+
192+ // VerifyBindEmail 验证邮箱绑定
193+ func (u * UserUsecase ) VerifyBindEmail (ctx context.Context , token string ) error {
194+ // 验证 token 的有效性(检查签名和过期时间)
195+ userIDStr , err := crypto .ValidateSimple (token )
196+ if err != nil {
197+ u .logger .WarnContext (ctx , "validate token failed" , "error" , err )
198+ return errcode .ErrEmailVerifyFailed .Wrap (err )
199+ }
200+
201+ userID , err := uuid .Parse (userIDStr )
202+ if err != nil {
203+ u .logger .WarnContext (ctx , "parse user id from token failed" , "error" , err )
204+ return errcode .ErrEmailVerifyFailed .Wrap (err )
205+ }
206+
207+ // 从 Redis 中取出存储的 token 和邮箱(一次性消费)
208+ key := fmt .Sprintf ("bind_email_token:%s" , userID .String ())
209+ redisValue , err := u .redis .GetDel (ctx , key ).Result ()
210+ if err != nil {
211+ if err == redis .Nil {
212+ return errcode .ErrEmailVerifyFailed
213+ }
214+ u .logger .ErrorContext (ctx , "get redis key failed" , "error" , err )
215+ return errcode .ErrDatabaseOperation .Wrap (err )
216+ }
217+
218+ // 解析 Redis 中的值:{token}:{email}
219+ parts := strings .SplitN (redisValue , ":" , 2 )
220+ if len (parts ) != 2 {
221+ u .logger .WarnContext (ctx , "invalid redis value format" , "value" , redisValue )
222+ return errcode .ErrEmailVerifyFailed
223+ }
224+
225+ storedToken := parts [0 ]
226+ email := parts [1 ]
227+
228+ // 验证 token 是否匹配(防止 token 替换)
229+ if storedToken != token {
230+ u .logger .WarnContext (ctx , "token mismatch" )
231+ return errcode .ErrEmailVerifyFailed
232+ }
233+
234+ // 再次检查邮箱是否被其他用户占用(防止竞态条件)
235+ existingUsers , err := u .repo .GetUserByEmail (ctx , []string {email })
236+ if err != nil && ! db .IsNotFound (err ) {
237+ return errcode .ErrDatabaseQuery .Wrap (err )
238+ }
239+ for _ , eu := range existingUsers {
240+ if eu .ID != userID {
241+ return errcode .ErrEmailTaken
242+ }
243+ }
244+
245+ // 更新用户邮箱
246+ if err := u .repo .SetEmail (ctx , userID , email ); err != nil {
247+ u .logger .ErrorContext (ctx , "set email failed" , "error" , err )
248+ return errcode .ErrDatabaseOperation .Wrap (err )
249+ }
250+
251+ u .logger .InfoContext (ctx , "bind email success" , "user_id" , userID , "email" , email )
252+ return nil
253+ }
0 commit comments