HaniBlindBox/.kiro/specs/user-auth-migration/design.md
2026-01-02 16:02:03 +08:00

20 KiB
Raw Blame History

Design Document: 用户认证系统迁移

Overview

本设计文档描述了将PHP用户认证系统迁移到.NET 8的技术方案。系统采用分层架构包括Controller层、Service层和Repository层。迁移过程中保持与现有UniApp前端的100%兼容性,使用相同的请求/响应格式。

Architecture

┌─────────────────────────────────────────────────────────────┐
│                      UniApp 前端                            │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    API Gateway / Nginx                       │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                   .NET 8 API 服务                           │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                  Controllers                         │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │   │
│  │  │AuthController│  │UserController│  │HealthCheck │  │   │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  │   │
│  └─────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                   Services                           │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │   │
│  │  │ AuthService │  │ UserService │  │ JwtService  │  │   │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │   │
│  │  │WechatService│  │ SmsService  │  │IpLocationSvc│  │   │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  │   │
│  └─────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                Infrastructure                        │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │   │
│  │  │  EF Core    │  │   Redis     │  │ COS Upload  │  │   │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                    │              │
                    ▼              ▼
            ┌───────────┐  ┌───────────┐
            │   MySQL   │  │   Redis   │
            └───────────┘  └───────────┘

Components and Interfaces

1. AuthController

负责处理所有认证相关的HTTP请求。

[ApiController]
[Route("api")]
public class AuthController : ControllerBase
{
    // POST /login - 微信小程序登录
    [HttpPost("login")]
    public async Task<ApiResponse<string>> WechatMiniProgramLogin(WechatLoginRequest request);
    
    // POST /mobileLogin - 手机号验证码登录
    [HttpPost("mobileLogin")]
    public async Task<ApiResponse<string>> MobileLogin(MobileLoginRequest request);
    
    // POST /login_bind_mobile - 微信授权绑定手机号
    [HttpPost("login_bind_mobile")]
    public async Task<ApiResponse<BindMobileResponse>> LoginBindMobile(BindMobileRequest request);
    
    // POST /bindMobile - 验证码绑定手机号
    [HttpPost("bindMobile")]
    public async Task<ApiResponse<BindMobileResponse>> BindMobile(BindMobileWithCodeRequest request);
    
    // GET|POST /login_record - 记录用户登录
    [HttpPost("login_record")]
    [HttpGet("login_record")]
    public async Task<ApiResponse<RecordLoginResponse>> RecordLogin(RecordLoginRequest request);
}

2. UserController

负责处理用户信息相关的HTTP请求。

[ApiController]
[Route("api")]
public class UserController : ControllerBase
{
    // POST /user - 获取用户信息
    [HttpPost("user")]
    public async Task<ApiResponse<UserInfoResponse>> GetUserInfo();
    
    // POST /update_userinfo - 更新用户信息
    [HttpPost("update_userinfo")]
    public async Task<ApiResponse> UpdateUserInfo(UpdateUserInfoRequest request);
    
    // POST /user_log_off - 注销账号
    [HttpPost("user_log_off")]
    public async Task<ApiResponse> LogOff(LogOffRequest request);
}

3. IAuthService

认证服务接口。

public interface IAuthService
{
    Task<LoginResult> WechatMiniProgramLoginAsync(string code, int? pid, string clickId);
    Task<LoginResult> MobileLoginAsync(string mobile, string code, int? pid, string clickId);
    Task<BindMobileResult> BindMobileAsync(int userId, string mobile, string code);
    Task<BindMobileResult> WechatBindMobileAsync(int userId, string wechatCode);
    Task RecordLoginAsync(int userId, string device, string deviceInfo);
    Task LogOffAsync(int userId, int type);
}

4. IWechatService

微信服务接口。

public interface IWechatService
{
    Task<WechatAuthResult> GetOpenIdAsync(string code);
    Task<WechatMobileResult> GetMobileAsync(string code);
}

5. IJwtService

JWT服务接口。

public interface IJwtService
{
    string GenerateToken(User user);
    ClaimsPrincipal ValidateToken(string token);
    int? GetUserIdFromToken(string token);
}

6. IUserService

用户服务接口。

public interface IUserService
{
    Task<User> GetUserByIdAsync(int userId);
    Task<User> GetUserByOpenIdAsync(string openId);
    Task<User> GetUserByUnionIdAsync(string unionId);
    Task<User> GetUserByMobileAsync(string mobile);
    Task<User> CreateUserAsync(CreateUserDto dto);
    Task UpdateUserAsync(int userId, UpdateUserDto dto);
    Task<UserInfoDto> GetUserInfoAsync(int userId);
    Task<int> CalculateVipLevelAsync(int userId, int currentVip);
}

7. IIpLocationService

IP地理位置服务接口。

public interface IIpLocationService
{
    Task<IpLocationResult> GetLocationAsync(string ip);
}

Data Models

Request Models

public class WechatLoginRequest
{
    public string Code { get; set; }
    public int? Pid { get; set; }
}

public class MobileLoginRequest
{
    public string Mobile { get; set; }
    public string Code { get; set; }
    public int? Pid { get; set; }
}

public class BindMobileRequest
{
    public string Code { get; set; }  // 微信授权code
}

public class BindMobileWithCodeRequest
{
    public string Mobile { get; set; }
    public string Code { get; set; }  // 短信验证码
}

public class UpdateUserInfoRequest
{
    public string Nickname { get; set; }
    public string Headimg { get; set; }
    public string Imagebase { get; set; }  // Base64图片
}

public class RecordLoginRequest
{
    public string Device { get; set; }
    public string DeviceInfo { get; set; }
}

public class LogOffRequest
{
    public int Type { get; set; }  // 0-注销 1-取消注销
}

Response Models

public class ApiResponse<T>
{
    public int Status { get; set; }  // 1成功 0失败 -1未登录
    public string Msg { get; set; }
    public T Data { get; set; }
}

public class UserInfoResponse
{
    public UserInfoDto Userinfo { get; set; }
    public OtherConfigDto Other { get; set; }
    public List<TaskDto> TaskList { get; set; }
}

public class UserInfoDto
{
    public int Id { get; set; }
    public string Uid { get; set; }
    public string Nickname { get; set; }
    public string Headimg { get; set; }
    public string Mobile { get; set; }  // 脱敏后的手机号
    public int MobileIs { get; set; }   // 是否绑定手机
    public decimal Money { get; set; }
    public decimal Money2 { get; set; }
    public decimal Integral { get; set; }
    public decimal Score { get; set; }
    public int Vip { get; set; }
    public string VipImgurl { get; set; }
    public int Coupon { get; set; }
    public int Day { get; set; }  // 注册天数
}

public class BindMobileResponse
{
    public string Token { get; set; }  // 账户合并时返回新token
}

public class RecordLoginResponse
{
    public string Uid { get; set; }
    public string Nickname { get; set; }
    public string Headimg { get; set; }
}

Internal DTOs

public class LoginResult
{
    public bool Success { get; set; }
    public string Token { get; set; }
    public int UserId { get; set; }
    public string ErrorMessage { get; set; }
}

public class WechatAuthResult
{
    public bool Success { get; set; }
    public string OpenId { get; set; }
    public string UnionId { get; set; }
    public string ErrorMessage { get; set; }
}

public class IpLocationResult
{
    public bool Success { get; set; }
    public string Province { get; set; }
    public string City { get; set; }
    public string Adcode { get; set; }
}

public class CreateUserDto
{
    public string OpenId { get; set; }
    public string UnionId { get; set; }
    public string Mobile { get; set; }
    public string Nickname { get; set; }
    public string Headimg { get; set; }
    public int Pid { get; set; }
    public int? ClickId { get; set; }
}

Key Implementation Details

1. 防抖机制实现

使用Redis实现登录防抖防止用户频繁请求

public async Task<bool> TryAcquireLoginLockAsync(string key, int expireSeconds = 3)
{
    var lockKey = $"login:debounce:{key}";
    return await _redis.StringSetAsync(lockKey, "1", TimeSpan.FromSeconds(expireSeconds), When.NotExists);
}

2. 用户UID生成策略

根据配置支持三种UID生成方式

  • 类型0: 使用真实用户ID
  • 类型1: 生成指定长度的纯数字ID
  • 类型2: 生成字母数字混合ID
public string GenerateUid(int userId, UidConfig config)
{
    return config.UidType switch
    {
        0 => userId.ToString(),
        1 => GenerateNumericUid(config.UidLength),
        2 => GenerateAlphaNumericUid(config.UidLength),
        _ => userId.ToString()
    };
}

3. 账户合并逻辑

当用户绑定的手机号已被其他账户使用时,需要合并账户:

public async Task<BindMobileResult> MergeAccountsAsync(int currentUserId, int mobileUserId)
{
    // 1. 将当前用户的openid迁移到手机号用户
    // 2. 生成新的token
    // 3. 删除当前用户和账户记录
    // 4. 返回新token
}

4. Token兼容策略

为保持与旧系统兼容同时使用JWT和数据库token

public async Task<string> CreateAccountTokenAsync(int userId)
{
    var tokenNum = GenerateRandomString(10);
    var time = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
    var accountToken = ComputeMd5($"{userId}{tokenNum}{time}");
    
    // 存储到UserAccount表
    await UpdateUserAccountAsync(userId, accountToken, tokenNum, time);
    
    return accountToken;
}

5. 默认头像生成

使用Identicon库生成用户默认头像

public async Task<(string nickname, string headimg)> CreateDefaultAvatarAsync(string seed, string prefix)
{
    var nickname = $"{prefix}{Random.Shared.Next(1000, 9999)}";
    var identicon = new Identicon(seed + nickname);
    var imageData = identicon.GetImageData();
    var url = await _cosUploader.UploadAsync(imageData, $"storage/users/icon/default/{Guid.NewGuid()}.png");
    return (nickname, url);
}

Correctness Properties

A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.

Property 1: 微信登录用户查找优先级

For any 用户登录请求如果提供了unionid系统应优先通过unionid查找用户只有当unionid查找失败时才通过openid查找。

Validates: Requirements 1.2

Property 2: 新用户创建完整性

For any 不存在的用户无论是微信登录还是手机号登录系统创建的新用户记录应包含有效的uid、非空的昵称、有效的头像URL、正确的pid如果提供

Validates: Requirements 1.3, 2.3

Property 3: 登录成功Token生成

For any 成功的登录请求微信或手机号系统应返回包含用户ID的有效JWT Token且该Token应使用配置的密钥正确签名。

Validates: Requirements 1.5, 2.4, 3.1, 3.2

Property 4: 登录防抖机制

For any 用户在3秒内发起的重复登录请求第二次及之后的请求应被拒绝并返回"请勿频繁登录"错误。

Validates: Requirements 1.6, 2.6

Property 5: 推荐关系记录

For any 新用户注册时提供的有效推荐人ID(pid)系统应将该pid正确保存到用户记录中。

Validates: Requirements 1.8, 2.7

Property 6: 验证码验证与清理

For any 手机号登录请求如果验证码正确系统应验证通过并删除Redis中的验证码如果验证码错误或过期系统应返回"验证码错误"。

Validates: Requirements 2.1, 2.2

Property 7: Token验证与授权

For any 携带Token的API请求如果Token有效系统应允许访问如果Token无效或过期系统应返回状态码-1未登录

Validates: Requirements 3.3, 3.4

Property 8: 数据库Token兼容存储

For any 成功的登录请求系统应同时在UserAccount表中存储account_token确保与旧系统兼容。

Validates: Requirements 3.6

Property 9: 用户信息完整性

For any 用户信息查询请求返回的数据应包含所有必要字段id、uid、nickname、headimg、mobile、money、integral、vip等且手机号应进行脱敏处理格式138****8000

Validates: Requirements 4.1, 4.4

Property 10: 昵称更新验证

For any 昵称更新请求,如果昵称为空,系统应拒绝更新;如果昵称有效,系统应保存更新。

Validates: Requirements 4.2

Property 11: VIP等级计算

For any 用户信息查询系统应根据用户的累计消费金额正确计算并返回VIP等级。

Validates: Requirements 4.5

Property 12: 手机号绑定验证码验证

For any 手机号绑定请求,系统应验证短信验证码;验证码错误时应返回"验证码错误"。

Validates: Requirements 5.1, 5.5

Property 13: 账户合并正确性

For any 手机号已被其他用户绑定的情况系统应正确合并账户将当前用户的openid迁移到手机号用户删除当前用户记录并返回新的Token。

Validates: Requirements 5.2, 5.3

Property 14: 直接绑定手机号

For any 手机号未被其他用户绑定的情况,系统应直接更新当前用户的手机号。

Validates: Requirements 5.4

Property 15: 登录日志记录

For any 成功的登录请求系统应在UserLoginLog表中记录登录日志并更新UserAccount表中的最后登录时间和IP信息。

Validates: Requirements 6.1, 6.3

Property 16: recordLogin接口返回值

For any recordLogin接口调用系统应返回用户的uid、昵称和头像。

Validates: Requirements 6.4

Property 17: 账号注销类型处理

For any 账号注销请求type=0时表示注销账号type=1时表示取消注销系统应记录相应的日志并返回成功消息。

Validates: Requirements 7.1, 7.2, 7.3

Error Handling

1. 微信API错误

  • 当微信API返回错误时记录详细错误日志
  • 返回用户友好的错误信息:"登录失败,请稍后重试"

2. 验证码错误

  • 验证码不存在或已过期:返回"验证码错误"
  • 验证码不匹配:返回"验证码错误"

3. 防抖限制

  • 用户频繁请求:返回"请勿频繁登录,请稍后再试"

4. Token错误

  • Token无效返回status=-1msg="未登录"
  • Token过期返回status=-1msg="登录已过期"

5. 数据库错误

  • 事务失败时回滚
  • 记录详细错误日志
  • 返回"网络故障,请稍后再试"

Testing Strategy

单元测试

使用xUnit框架编写单元测试覆盖以下场景

  1. AuthService测试

    • 微信登录成功/失败场景
    • 手机号登录成功/失败场景
    • 防抖机制测试
    • 账户合并测试
  2. JwtService测试

    • Token生成测试
    • Token验证测试
    • Token过期测试
  3. UserService测试

    • 用户查找测试
    • 用户创建测试
    • 用户信息更新测试
    • VIP等级计算测试

属性测试

使用FsCheck库进行属性测试每个属性测试运行至少100次迭代

  1. Property 1-5: 登录相关属性测试
  2. Property 6-8: Token相关属性测试
  3. Property 9-11: 用户信息相关属性测试
  4. Property 12-14: 手机号绑定相关属性测试
  5. Property 15-17: 日志和注销相关属性测试

集成测试

  1. API端到端测试

    • 完整的微信登录流程
    • 完整的手机号登录流程
    • 用户信息获取和更新流程
  2. 数据库集成测试

    • 用户创建和查询
    • 账户合并
    • 登录日志记录

测试配置

// 属性测试配置示例
[Property(MaxTest = 100)]
public Property LoginShouldGenerateValidToken()
{
    return Prop.ForAll(
        Arb.From<ValidLoginRequest>(),
        request => {
            var result = _authService.LoginAsync(request).Result;
            return result.Success && !string.IsNullOrEmpty(result.Token);
        });
}