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:
parent
ee4cac59c7
commit
d4b802502e
|
|
@ -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)}`,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -256,35 +256,38 @@ 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(
|
||||||
width: nicknameAreaWidth,
|
padding: const EdgeInsets.only(bottom: 38), // 火柴人高度36 + 间距2
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
width: nicknameAreaWidth,
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
color: data.isCurrentUser
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||||
? const Color(0xFFD4AF37).withValues(alpha: 0.2)
|
decoration: BoxDecoration(
|
||||||
: 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
|
color: data.isCurrentUser
|
||||||
? const Color(0xFFD4AF37)
|
? const Color(0xFFD4AF37).withValues(alpha: 0.2)
|
||||||
: const Color(0xFF5D4037),
|
: 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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue