# 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请求。 ```csharp [ApiController] [Route("api")] public class AuthController : ControllerBase { // POST /login - 微信小程序登录 [HttpPost("login")] public async Task> WechatMiniProgramLogin(WechatLoginRequest request); // POST /mobileLogin - 手机号验证码登录 [HttpPost("mobileLogin")] public async Task> MobileLogin(MobileLoginRequest request); // POST /login_bind_mobile - 微信授权绑定手机号 [HttpPost("login_bind_mobile")] public async Task> LoginBindMobile(BindMobileRequest request); // POST /bindMobile - 验证码绑定手机号 [HttpPost("bindMobile")] public async Task> BindMobile(BindMobileWithCodeRequest request); // GET|POST /login_record - 记录用户登录 [HttpPost("login_record")] [HttpGet("login_record")] public async Task> RecordLogin(RecordLoginRequest request); } ``` ### 2. UserController 负责处理用户信息相关的HTTP请求。 ```csharp [ApiController] [Route("api")] public class UserController : ControllerBase { // POST /user - 获取用户信息 [HttpPost("user")] public async Task> GetUserInfo(); // POST /update_userinfo - 更新用户信息 [HttpPost("update_userinfo")] public async Task UpdateUserInfo(UpdateUserInfoRequest request); // POST /user_log_off - 注销账号 [HttpPost("user_log_off")] public async Task LogOff(LogOffRequest request); } ``` ### 3. IAuthService 认证服务接口。 ```csharp public interface IAuthService { Task WechatMiniProgramLoginAsync(string code, int? pid, string clickId); Task MobileLoginAsync(string mobile, string code, int? pid, string clickId); Task BindMobileAsync(int userId, string mobile, string code); Task WechatBindMobileAsync(int userId, string wechatCode); Task RecordLoginAsync(int userId, string device, string deviceInfo); Task LogOffAsync(int userId, int type); } ``` ### 4. IWechatService 微信服务接口。 ```csharp public interface IWechatService { Task GetOpenIdAsync(string code); Task GetMobileAsync(string code); } ``` ### 5. IJwtService JWT服务接口。 ```csharp public interface IJwtService { string GenerateToken(User user); ClaimsPrincipal ValidateToken(string token); int? GetUserIdFromToken(string token); } ``` ### 6. IUserService 用户服务接口。 ```csharp public interface IUserService { Task GetUserByIdAsync(int userId); Task GetUserByOpenIdAsync(string openId); Task GetUserByUnionIdAsync(string unionId); Task GetUserByMobileAsync(string mobile); Task CreateUserAsync(CreateUserDto dto); Task UpdateUserAsync(int userId, UpdateUserDto dto); Task GetUserInfoAsync(int userId); Task CalculateVipLevelAsync(int userId, int currentVip); } ``` ### 7. IIpLocationService IP地理位置服务接口。 ```csharp public interface IIpLocationService { Task GetLocationAsync(string ip); } ``` ## Data Models ### Request Models ```csharp 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 ```csharp public class ApiResponse { 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 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 ```csharp 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实现登录防抖,防止用户频繁请求: ```csharp public async Task 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 ```csharp 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. 账户合并逻辑 当用户绑定的手机号已被其他账户使用时,需要合并账户: ```csharp public async Task MergeAccountsAsync(int currentUserId, int mobileUserId) { // 1. 将当前用户的openid迁移到手机号用户 // 2. 生成新的token // 3. 删除当前用户和账户记录 // 4. 返回新token } ``` ### 4. Token兼容策略 为保持与旧系统兼容,同时使用JWT和数据库token: ```csharp public async Task 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库生成用户默认头像: ```csharp 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=-1,msg="未登录" - Token过期:返回status=-1,msg="登录已过期" ### 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. **数据库集成测试** - 用户创建和查询 - 账户合并 - 登录日志记录 ### 测试配置 ```csharp // 属性测试配置示例 [Property(MaxTest = 100)] public Property LoginShouldGenerateValidToken() { return Prop.ForAll( Arb.From(), request => { var result = _authService.LoginAsync(request).Result; return result.Success && !string.IsNullOrEmpty(result.Token); }); } ```