diff --git a/src/Jiaowu.Api/Controllers/UsersController.cs b/src/Jiaowu.Api/Controllers/UsersController.cs index 83c987b..83bedfb 100644 --- a/src/Jiaowu.Api/Controllers/UsersController.cs +++ b/src/Jiaowu.Api/Controllers/UsersController.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using MySql.Data.MySqlClient; namespace Jiaowu.Api.Controllers; @@ -33,6 +34,14 @@ public sealed class UsersController( if (page < 1 || pageSize is < 1 or > 100) return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。"); + // Connector/NET 10.0.9 currently has a provider-side command creation + // failure with this Identity query on EF Core 10.0.11. Keep the normal + // LINQ path for SQLite/tests and bypass only this affected MySQL read. + if (db.Database.ProviderName?.Contains("MySql", StringComparison.OrdinalIgnoreCase) == true) + return Ok(await GetUsersFromMySqlAsync( + page, pageSize, keyword, roleName, collegeId, isEnabled, + hasLoggedIn, cancellationToken)); + var query = userManager.Users.AsNoTracking(); if (!string.IsNullOrWhiteSpace(keyword)) { @@ -111,6 +120,107 @@ public sealed class UsersController( return Ok(new PagedResult(items, total, page, pageSize)); } + private async Task> GetUsersFromMySqlAsync( + int page, + int pageSize, + string? keyword, + string? roleName, + Guid? collegeId, + bool? isEnabled, + bool? hasLoggedIn, + CancellationToken cancellationToken) + { + var clauses = new List(); + if (!string.IsNullOrWhiteSpace(keyword)) + clauses.Add("(" + + "u.`UserName` LIKE CONCAT('%', @keyword, '%') OR " + + "u.`DisplayName` LIKE CONCAT('%', @keyword, '%') OR " + + "u.`StaffNumber` LIKE CONCAT('%', @keyword, '%') OR " + + "EXISTS (SELECT 1 FROM `AspNetUserRoles` ur " + + "JOIN `AspNetRoles` r ON ur.`RoleId` = r.`Id` " + + "WHERE ur.`UserId` = u.`Id` AND r.`Name` LIKE CONCAT('%', @keyword, '%')))" ); + if (!string.IsNullOrWhiteSpace(roleName)) + clauses.Add("EXISTS (SELECT 1 FROM `AspNetUserRoles` ur " + + "JOIN `AspNetRoles` r ON ur.`RoleId` = r.`Id` " + + "WHERE ur.`UserId` = u.`Id` AND r.`Name` = @roleName)"); + if (collegeId.HasValue) clauses.Add("u.`CollegeId` = @collegeId"); + if (isEnabled.HasValue) clauses.Add("u.`IsEnabled` = @isEnabled"); + if (hasLoggedIn.HasValue) clauses.Add(hasLoggedIn.Value + ? "u.`LastLoginAt` IS NOT NULL" : "u.`LastLoginAt` IS NULL"); + var where = clauses.Count == 0 ? string.Empty : " WHERE " + string.Join(" AND ", clauses); + + var connection = (MySqlConnection)db.Database.GetDbConnection(); + await connection.OpenAsync(cancellationToken); + try + { + await using var countCommand = CreateCommand( + connection, "SELECT COUNT(*) FROM `AspNetUsers` u" + where, + keyword, roleName, collegeId, isEnabled); + var total = Convert.ToInt32(await countCommand.ExecuteScalarAsync(cancellationToken)); + + await using var usersCommand = CreateCommand(connection, + "SELECT u.`Id`, u.`UserName`, u.`DisplayName`, u.`StaffNumber`, u.`CollegeId`, " + + "u.`IsEnabled`, u.`LastLoginAt`, u.`CreatedAt` FROM `AspNetUsers` u" + where + + " ORDER BY u.`UserName`, u.`Id` LIMIT @take OFFSET @skip", + keyword, roleName, collegeId, isEnabled); + usersCommand.Parameters.AddWithValue("@take", pageSize); + usersCommand.Parameters.AddWithValue("@skip", (page - 1) * pageSize); + var users = new List(); + await using (var reader = await usersCommand.ExecuteReaderAsync(cancellationToken)) + { + while (await reader.ReadAsync(cancellationToken)) + { + var id = Guid.Parse(reader.GetString(0)); + users.Add(new UserListItem(id, reader.GetString(1), reader.GetString(2), + reader.IsDBNull(3) ? null : reader.GetString(3), + reader.IsDBNull(4) ? null : Guid.Parse(reader.GetString(4)), + Convert.ToBoolean(reader.GetValue(5)), + reader.IsDBNull(6) ? null : reader.GetDateTime(6), + reader.GetDateTime(7), Array.Empty())); + } + } + + var rolesByUser = new Dictionary>(); + if (users.Count > 0) + { + await using var rolesCommand = new MySqlCommand( + "SELECT ur.`UserId`, r.`Name` FROM `AspNetUserRoles` ur " + + "JOIN `AspNetRoles` r ON ur.`RoleId` = r.`Id` WHERE ur.`UserId` IN (" + + string.Join(", ", users.Select((_, index) => "@user" + index)) + ")", connection); + foreach (var (user, index) in users.Select((user, index) => (user, index))) + rolesCommand.Parameters.AddWithValue("@user" + index, user.Id.ToString()); + await using var roleReader = await rolesCommand.ExecuteReaderAsync(cancellationToken); + while (await roleReader.ReadAsync(cancellationToken)) + { + var userId = Guid.Parse(roleReader.GetString(0)); + if (!rolesByUser.TryGetValue(userId, out var roles)) rolesByUser[userId] = roles = []; + roles.Add(roleReader.GetString(1)); + } + } + var items = users.Select(user => user with + { + Roles = rolesByUser.GetValueOrDefault(user.Id, []).OrderBy(x => x).ToArray() + }).ToArray(); + return new PagedResult(items, total, page, pageSize); + } + finally + { + await connection.CloseAsync(); + } + } + + private static MySqlCommand CreateCommand( + MySqlConnection connection, string sql, string? keyword, string? roleName, + Guid? collegeId, bool? isEnabled) + { + var command = new MySqlCommand(sql, connection); + if (!string.IsNullOrWhiteSpace(keyword)) command.Parameters.AddWithValue("@keyword", keyword.Trim()); + if (!string.IsNullOrWhiteSpace(roleName)) command.Parameters.AddWithValue("@roleName", roleName.Trim()); + if (collegeId.HasValue) command.Parameters.AddWithValue("@collegeId", collegeId.Value.ToString()); + if (isEnabled.HasValue) command.Parameters.AddWithValue("@isEnabled", isEnabled.Value); + return command; + } + [HttpGet("roles")] public async Task> GetRoles(CancellationToken cancellationToken) => Ok(await roleManager.Roles.AsNoTracking()