fix: 修复验证码竞态条件和火柴人对齐问题

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 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-24 03:54:25 -08:00
parent ee4cac59c7
commit d4b802502e
2 changed files with 41 additions and 32 deletions

View File

@ -648,8 +648,10 @@ export class UserApplicationService {
const code = this.generateSmsCode(); const code = this.generateSmsCode();
const cacheKey = `sms:${command.type.toLowerCase()}:${phoneNumber.value}`; 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.redisService.set(cacheKey, code, 300);
await this.smsService.sendVerificationCode(phoneNumber.value, code);
} }
async register(command: RegisterCommand): Promise<RegisterResult> { async register(command: RegisterCommand): Promise<RegisterResult> {
@ -2058,8 +2060,9 @@ export class UserApplicationService {
const code = this.generateSmsCode(); const code = this.generateSmsCode();
const cacheKey = `sms:withdraw:${user.phoneNumber.value}`; 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.redisService.set(cacheKey, code, 300); // 5分钟有效
await this.smsService.sendVerificationCode(user.phoneNumber.value, code);
this.logger.log( this.logger.log(
`Withdraw SMS code sent successfully to: ${this.maskPhoneNumber(user.phoneNumber.value)}`, `Withdraw SMS code sent successfully to: ${this.maskPhoneNumber(user.phoneNumber.value)}`,
@ -2267,8 +2270,9 @@ export class UserApplicationService {
const code = this.generateSmsCode(); const code = this.generateSmsCode();
const cacheKey = `sms:reset_password:${phone.value}`; 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.redisService.set(cacheKey, code, 300); // 5分钟有效
await this.smsService.sendVerificationCode(phone.value, code);
this.logger.log( this.logger.log(
`[RESET_PASSWORD] SMS code sent successfully to: ${this.maskPhoneNumber(phoneNumber)}`, `[RESET_PASSWORD] SMS code sent successfully to: ${this.maskPhoneNumber(phoneNumber)}`,
@ -2414,6 +2418,9 @@ export class UserApplicationService {
const code = this.generateEmailCode(); const code = this.generateEmailCode();
const cacheKey = `email:${command.purpose.toLowerCase()}:${command.email.toLowerCase()}`; const cacheKey = `email:${command.purpose.toLowerCase()}:${command.email.toLowerCase()}`;
// 先缓存验证码,再发送邮件(避免竞态条件)
await this.redisService.set(cacheKey, code, 300); // 5分钟有效
// 发送验证码邮件 // 发送验证码邮件
const result = await this.emailService.sendVerificationCode( const result = await this.emailService.sendVerificationCode(
command.email, command.email,
@ -2421,12 +2428,11 @@ export class UserApplicationService {
); );
if (!result.success) { if (!result.success) {
// 发送失败时删除缓存的验证码
await this.redisService.delete(cacheKey);
throw new ApplicationError(`发送验证码失败: ${result.message}`); throw new ApplicationError(`发送验证码失败: ${result.message}`);
} }
// 缓存验证码5分钟有效
await this.redisService.set(cacheKey, code, 300);
this.logger.log( this.logger.log(
`Email code sent successfully to: ${this.maskEmail(command.email)}`, `Email code sent successfully to: ${this.maskEmail(command.email)}`,
); );

View File

@ -256,10 +256,12 @@ class _StickmanRaceWidgetState extends State<StickmanRaceWidget>
top: verticalPosition - bounce, top: verticalPosition - bounce,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, //
children: [ children: [
// - // -
SizedBox( Padding(
padding: const EdgeInsets.only(bottom: 38), // 36 + 2
child: SizedBox(
width: nicknameAreaWidth, width: nicknameAreaWidth,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
@ -288,6 +290,7 @@ class _StickmanRaceWidgetState extends State<StickmanRaceWidget>
), ),
), ),
), ),
),
// //
SizedBox( SizedBox(
width: stickmanBoxWidth.clamp(60.0, double.infinity), width: stickmanBoxWidth.clamp(60.0, double.infinity),