From d4b802502ec29b56002c3576bac57c6bc004865a Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 24 Dec 2025 03:54:25 -0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E7=A0=81=E7=AB=9E=E6=80=81=E6=9D=A1=E4=BB=B6=E5=92=8C=E7=81=AB?= =?UTF-8?q?=E6=9F=B4=E4=BA=BA=E5=AF=B9=E9=BD=90=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修复验证码发送的竞态条件: - 调整执行顺序,先存储验证码到 Redis,再发送短信/邮件 - 避免用户收到验证码后立即输入但 Redis 中尚未存储的情况 - 影响:sendSmsCode、sendWithdrawSmsCode、sendResetPasswordSmsCode、sendEmailCode 2. 修复火柴人组件昵称与数量标签对齐问题: - 使用底部对齐 + Padding 让昵称标签与数量标签在同一水平线 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../services/user-application.service.ts | 18 ++++-- .../widgets/stickman_race_widget.dart | 55 ++++++++++--------- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/backend/services/identity-service/src/application/services/user-application.service.ts b/backend/services/identity-service/src/application/services/user-application.service.ts index ce9f64ab..d6f8265e 100644 --- a/backend/services/identity-service/src/application/services/user-application.service.ts +++ b/backend/services/identity-service/src/application/services/user-application.service.ts @@ -648,8 +648,10 @@ export class UserApplicationService { const code = this.generateSmsCode(); const cacheKey = `sms:${command.type.toLowerCase()}:${phoneNumber.value}`; - await this.smsService.sendVerificationCode(phoneNumber.value, code); + // 先存储到 Redis,再发送短信 + // 避免短信发送过程中用户已收到验证码但 Redis 中还没有存储的竞态条件 await this.redisService.set(cacheKey, code, 300); + await this.smsService.sendVerificationCode(phoneNumber.value, code); } async register(command: RegisterCommand): Promise { @@ -2058,8 +2060,9 @@ export class UserApplicationService { const code = this.generateSmsCode(); const cacheKey = `sms:withdraw:${user.phoneNumber.value}`; - await this.smsService.sendVerificationCode(user.phoneNumber.value, code); + // 先存储到 Redis,再发送短信(避免竞态条件) await this.redisService.set(cacheKey, code, 300); // 5分钟有效 + await this.smsService.sendVerificationCode(user.phoneNumber.value, code); this.logger.log( `Withdraw SMS code sent successfully to: ${this.maskPhoneNumber(user.phoneNumber.value)}`, @@ -2267,8 +2270,9 @@ export class UserApplicationService { const code = this.generateSmsCode(); const cacheKey = `sms:reset_password:${phone.value}`; - await this.smsService.sendVerificationCode(phone.value, code); + // 先存储到 Redis,再发送短信(避免竞态条件) await this.redisService.set(cacheKey, code, 300); // 5分钟有效 + await this.smsService.sendVerificationCode(phone.value, code); this.logger.log( `[RESET_PASSWORD] SMS code sent successfully to: ${this.maskPhoneNumber(phoneNumber)}`, @@ -2414,6 +2418,9 @@ export class UserApplicationService { const code = this.generateEmailCode(); const cacheKey = `email:${command.purpose.toLowerCase()}:${command.email.toLowerCase()}`; + // 先缓存验证码,再发送邮件(避免竞态条件) + await this.redisService.set(cacheKey, code, 300); // 5分钟有效 + // 发送验证码邮件 const result = await this.emailService.sendVerificationCode( command.email, @@ -2421,12 +2428,11 @@ export class UserApplicationService { ); if (!result.success) { + // 发送失败时删除缓存的验证码 + await this.redisService.delete(cacheKey); throw new ApplicationError(`发送验证码失败: ${result.message}`); } - // 缓存验证码,5分钟有效 - await this.redisService.set(cacheKey, code, 300); - this.logger.log( `Email code sent successfully to: ${this.maskEmail(command.email)}`, ); diff --git a/frontend/mobile-app/lib/features/authorization/presentation/widgets/stickman_race_widget.dart b/frontend/mobile-app/lib/features/authorization/presentation/widgets/stickman_race_widget.dart index 7990ec3e..802d6aad 100644 --- a/frontend/mobile-app/lib/features/authorization/presentation/widgets/stickman_race_widget.dart +++ b/frontend/mobile-app/lib/features/authorization/presentation/widgets/stickman_race_widget.dart @@ -256,35 +256,38 @@ class _StickmanRaceWidgetState extends State top: verticalPosition - bounce, child: Row( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, // 底部对齐 children: [ - // 昵称标签 - 固定在左边 - SizedBox( - width: nicknameAreaWidth, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), - decoration: BoxDecoration( - color: data.isCurrentUser - ? const Color(0xFFD4AF37).withValues(alpha: 0.2) - : Colors.white.withValues(alpha: 0.8), - borderRadius: BorderRadius.circular(4), - border: data.isCurrentUser - ? Border.all(color: const Color(0xFFD4AF37), width: 1) - : null, - ), - child: Text( - data.nickname, - style: TextStyle( - fontSize: 9, - fontFamily: 'Inter', - fontWeight: - data.isCurrentUser ? FontWeight.w600 : FontWeight.w400, + // 昵称标签 - 固定在左边,与数量标签底部对齐 + Padding( + padding: const EdgeInsets.only(bottom: 38), // 火柴人高度36 + 间距2 + child: SizedBox( + width: nicknameAreaWidth, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( color: data.isCurrentUser - ? const Color(0xFFD4AF37) - : const Color(0xFF5D4037), + ? const Color(0xFFD4AF37).withValues(alpha: 0.2) + : Colors.white.withValues(alpha: 0.8), + borderRadius: BorderRadius.circular(4), + border: data.isCurrentUser + ? Border.all(color: const Color(0xFFD4AF37), width: 1) + : null, + ), + child: Text( + data.nickname, + style: TextStyle( + fontSize: 9, + fontFamily: 'Inter', + fontWeight: + data.isCurrentUser ? FontWeight.w600 : FontWeight.w400, + color: data.isCurrentUser + ? const Color(0xFFD4AF37) + : const Color(0xFF5D4037), + ), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, ), - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, ), ), ),