using MAX.Framework.Validation; using FreeSql; using MAX.Core.DependencyInjection; using MAX.Framework.Commons; using MAX.Framework.Security; using Store.Users.Contracts; using Store.Users.Contracts.Dtos; using Store.Users.Entities; using Store.Users.Mappers; namespace Store.Users.Services; /// 账户管理用例:所有操作从认证上下文确定操作者,禁止相信客户端传入的父账号或管理员标记。 public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser currentUser, IUserQueryService query, IOnlineUserStore onlineUsers) : ITransientDependency { private async Task Actor(IFreeSql db, CancellationToken ct) { ct.ThrowIfCancellationRequested(); if (!currentUser.IsAuthenticated || string.IsNullOrWhiteSpace(currentUser.UserName)) throw new UnauthorizedAccessException(); return await db.Select().Where(u => u.UserName == currentUser.UserName).FirstAsync(ct) ?? throw new UnauthorizedAccessException("登录账户不存在"); } private static bool IsAdmin(UserEntity actor) => UserAccessPolicy.IsAdmin(actor); /// 仅本人、直属主账号或超级管理员可以读取账户详情。 public async Task GetUser(string? userName, CancellationToken ct) { var actor = await Actor(orm, ct); var name = string.IsNullOrWhiteSpace(userName) ? actor.UserName : userName.Trim(); RequestValidator.Validate(new UserNameRequest { UserName = name }); UserInputChecks.Length(name, 20, "用户名"); var target = await orm.Select().Where(u => u.UserName == name).FirstAsync(ct) ?? throw new AppException("用户不存在"); if (!UserAccessPolicy.CanRead(actor, target)) throw new AppException("无权访问该账户", 403); return await query.GetUser(target.UserName); } /// 解析代操作目标,服务端校验管理员或主子账号归属,省略用户名时操作本人。 public async Task GetManagedUserName(string? userName, CancellationToken ct) { var actor = await Actor(orm, ct); var name = string.IsNullOrWhiteSpace(userName) ? actor.UserName : userName.Trim(); UserInputChecks.Length(name, 20, "用户名"); var target = await orm.Select().Where(u => u.UserName == name).FirstAsync(ct) ?? throw new AppException("用户不存在"); if (!UserAccessPolicy.CanManage(actor, target) && actor.Id != target.Id) throw new AppException("无权操作该账户", 403); return target.UserName; } /// 管理员查询全部账户;普通主账号的列表范围为本人和直属子账号。 public async Task> Search(UserSearchRequest request, bool subUsersOnly, CancellationToken ct) { RequestValidator.Validate(request); UserInputChecks.Search(request); var actor = await Actor(orm, ct); if (!subUsersOnly && !IsAdmin(actor) && !actor.IsMainUser) throw new AppException("无权查询账户列表", 403); if (subUsersOnly && !IsAdmin(actor) && !actor.IsMainUser) throw new AppException("子账号不能管理其他账户", 403); var select = orm.Select(); if (subUsersOnly) { var parentUserName = request.ParentUserName?.Trim(); if (IsAdmin(actor)) { // 管理员省略父账号时查询所有子账号,但不混入主账号。 select = select.Where(u => u.ParentUserName != null && u.ParentUserName.Trim() != ""); if (!string.IsNullOrWhiteSpace(parentUserName)) select = select.Where(u => u.ParentUserName == parentUserName); } else { if (!string.IsNullOrWhiteSpace(parentUserName) && !string.Equals(parentUserName, actor.UserName, StringComparison.OrdinalIgnoreCase)) throw new AppException("只能查询自己的子账号", 403); select = select.Where(u => u.ParentUserName == actor.UserName); } } else if (!IsAdmin(actor)) select = select.Where(u => u.Id == actor.Id || u.ParentUserName == actor.UserName); if (request.GroupId.HasValue) select = select.Where(u => u.GroupId == request.GroupId.Value); if (!string.IsNullOrWhiteSpace(request.Keyword)) { var keyword = request.Keyword.Trim(); select = select.Where(u => u.UserName.Contains(keyword) || u.NickName.Contains(keyword) || u.Mobile.Contains(keyword) || u.Notes.Contains(keyword)); } var total = await select.CountAsync(ct); var items = await select.OrderByDescending(u => u.Id).Page(request.Page, request.PageSize).ToListAsync(ct); return new UserPage(total, items.Select(u => new UserMapper().ToInfo(u)).ToList()); } /// 创建账户并初始化余额。主账号仅由管理员创建,子账号的归属由服务端指定。 public async Task Create(UserCreateRequest request, bool subUser, CancellationToken ct) { RequestValidator.Validate(request); UserInputChecks.Length(request.UserName, 20, "用户名"); UserInputChecks.Length(request.Password, 128, "密码", 6); UserInputChecks.Edit(request); UserInputChecks.Length(request.ParentUserName, 20, "父账号"); using var work = orm.CreateUnitOfWork(); work.GetOrBeginTransaction(); var db = work.Orm; var actor = await Actor(db, ct); // 锁住主账号,使其创建子账号和删除操作串行化。 actor = await db.Select().Where(u => u.Id == actor.Id).ForUpdate().FirstAsync(ct) ?? throw new AppException("操作者账户不存在"); if (subUser ? !IsAdmin(actor) && !actor.IsMainUser : !IsAdmin(actor)) throw new AppException("无权创建该账户", 403); var parentName = ""; if (subUser) { parentName = string.IsNullOrWhiteSpace(request.ParentUserName) ? actor.UserName : request.ParentUserName.Trim(); if (!IsAdmin(actor) && !string.Equals(parentName, actor.UserName, StringComparison.OrdinalIgnoreCase)) throw new AppException("只能在自己的账户下创建子账号", 403); var parent = await db.Select().Where(u => u.UserName == parentName).ForUpdate().FirstAsync(ct) ?? throw new AppException("父账号不存在"); if (!parent.IsMainUser) throw new AppException("父账号必须是主账号"); parentName = parent.UserName; } var name = request.UserName.Trim(); if (await db.Select().Where(u => u.UserName == name).AnyAsync(ct)) throw new AppException("用户名已存在"); var user = new UserEntity { UserName = name, ParentUserName = parentName, GroupId = subUser ? (byte)4 : (byte)3, NickName = name, Mobile = "", CompanyName = "", Notes = "", CardId = "", Authority = "", ForbiddenPage = "", OemId = "", SaLevel = 0, ShopPointsPerMonth = 30, PostagePerOrder = 2.2m, RefundPostagePerOrder = 2.5m, GiftOemAmountPerOrder = 0, SyncLogisticsPerOrder = 0, Points = 0, Balance = 0, RechargePointsTotal = 0, RechargeAmountTotal = 0, AddTime = DateTime.Now, MdyTime = DateTime.Now }; Apply(user, request, actor, subUser); await CheckOem(db, user, ct); user.Password = UserPassword.Hash(user, request.Password); user.Id = checked((int)await db.Insert(user).ExecuteIdentityAsync(ct)); ct.ThrowIfCancellationRequested(); work.Commit(); return await query.GetUser(user.UserName); } /// 修改资料。登录名保持稳定,避免破坏主子账号、商铺和历史流水中的关联。 public async Task Update(int id, UserEditRequest request, bool subUser, CancellationToken ct) { UserInputChecks.PositiveId(id); RequestValidator.Validate(request); UserInputChecks.Edit(request); using var work = orm.CreateUnitOfWork(); work.GetOrBeginTransaction(); var db = work.Orm; var actor = await Actor(db, ct); var target = await db.Select().Where(u => u.Id == id).ForUpdate().FirstAsync(ct) ?? throw new AppException("用户不存在"); if (!UserAccessPolicy.CanManage(actor, target)) throw new AppException("无权修改该账户", 403); if (subUser && target.IsMainUser) throw new AppException("目标账户不是子账号"); Apply(target, request, actor, !target.IsMainUser); await CheckOem(db, target, ct); target.MdyTime = DateTime.Now; // 仅写入资料列,不把读取到的余额和积分覆盖回数据库。 await db.Update().SetSource(target).UpdateColumns(u => new { u.NickName, u.Mobile, u.CompanyName, u.Notes, u.CardId, u.GroupId, u.SaLevel, u.Authority, u.ForbiddenPage, u.OemId, u.ShopPointsPerMonth, u.PostagePerOrder, u.RefundPostagePerOrder, u.GiftOemAmountPerOrder, u.SyncLogisticsPerOrder, u.MdyTime }).ExecuteAffrowsAsync(ct); ct.ThrowIfCancellationRequested(); work.Commit(); onlineUsers.SignOut(target.UserName); return await query.GetUser(target.UserName); } /// 只有无余额、无积分、无子账号且无商铺关联的账户才能删除,保留历史流水。 public async Task Delete(int id, CancellationToken ct) { UserInputChecks.PositiveId(id); using var work = orm.CreateUnitOfWork(); work.GetOrBeginTransaction(); var db = work.Orm; var actor = await Actor(db, ct); var target = await db.Select().Where(u => u.Id == id).ForUpdate().FirstAsync(ct) ?? throw new AppException("用户不存在"); if (actor.Id == target.Id) throw new AppException("不能删除当前登录账户"); if (!UserAccessPolicy.CanManage(actor, target)) throw new AppException("无权删除该账户", 403); if ((target.Balance ?? 0) != 0 || target.Points != 0) throw new AppException("账户仍有余额或积分,不能删除"); if (await db.Select().Where(u => u.ParentUserName == target.UserName).AnyAsync(ct)) throw new AppException("请先处理该账户的子账号"); if (await db.Select().Where(s => s.UserName == target.UserName).AnyAsync(ct)) throw new AppException("账户仍有关联店铺,不能删除"); if (await db.Delete().Where(u => u.Id == id && u.Points == 0 && (u.Balance == null || u.Balance == 0)) .ExecuteAffrowsAsync(ct) != 1) throw new AppException("账户状态已改变,请刷新后重试"); ct.ThrowIfCancellationRequested(); work.Commit(); onlineUsers.SignOut(target.UserName); } /// 本人改密校验原密码,管理者可重置授权账户密码;比较并更新后失效目标账户会话。 public async Task ChangePassword(ChangePasswordRequest request, CancellationToken ct) { RequestValidator.Validate(request); UserInputChecks.Length(request.OldPassword, 128, "原密码"); UserInputChecks.Length(request.NewPassword, 128, "新密码", 6); var actor = await Actor(orm, ct); var name = string.IsNullOrWhiteSpace(request.UserName) ? actor.UserName : request.UserName.Trim(); UserInputChecks.Length(name, 20, "用户名"); var target = await orm.Select().Where(u => u.UserName == name).FirstAsync(ct) ?? throw new AppException("用户不存在"); if (target.Id == actor.Id) { RequestValidator.Validate(new UserPasswordProofRequest { OldPassword = request.OldPassword! }); if (!UserPassword.Verify(target, request.OldPassword!)) throw new AppException("原密码错误"); } else if (!UserAccessPolicy.CanManage(actor, target)) throw new AppException("无权修改该账户密码", 403); var hash = UserPassword.Hash(target, request.NewPassword); if (await orm.Update().Set(u => u.Password, hash).Set(u => u.MdyTime, DateTime.Now) .Where(u => u.Id == target.Id && u.Password == target.Password).ExecuteAffrowsAsync(ct) != 1) throw new AppException("密码已发生变化,请重新登录"); onlineUsers.SignOut(target.UserName); } /// 按账户归属查询积分流水;分页与账户详情使用相同的访问范围。 public async Task> Points(string? userName, UserRecordSearchRequest request, CancellationToken ct) { RequestValidator.Validate(request); UserInputChecks.Records(request); var user = await GetUser(userName, ct); var select = orm.Select().Where(r => r.UserName == user.UserName); if (request.StartTime.HasValue) select = select.Where(r => r.AddTime >= request.StartTime.Value); if (request.EndTime.HasValue) select = select.Where(r => r.AddTime <= request.EndTime.Value); if (!string.IsNullOrWhiteSpace(request.Operation)) select = select.Where(r => r.OptionType == request.Operation); if (!string.IsNullOrWhiteSpace(request.ShopId)) select = select.Where(r => r.ShopId == request.ShopId); var total = await select.CountAsync(ct); var rows = await select.OrderByDescending(r => r.Id).Page(request.Page, request.PageSize).ToListAsync(ct); return new(total, rows); } /// 余额流水排除旧系统已标记删除的记录。 public async Task> Balance(string? userName, UserRecordSearchRequest request, CancellationToken ct) { RequestValidator.Validate(request); UserInputChecks.Records(request); var user = await GetUser(userName, ct); var select = orm.Select().Where(r => r.UserName == user.UserName && r.IsDeleted == 0); if (request.StartTime.HasValue) select = select.Where(r => r.CreateTime >= request.StartTime.Value); if (request.EndTime.HasValue) select = select.Where(r => r.CreateTime <= request.EndTime.Value); if (request.PaymentType.HasValue) select = select.Where(r => r.Type == request.PaymentType.Value); if (!string.IsNullOrWhiteSpace(request.OrderId)) select = select.Where(r => r.OrderId == request.OrderId); var total = await select.CountAsync(ct); var rows = await select.OrderByDescending(r => r.Id).Page(request.Page, request.PageSize).ToListAsync(ct); return new(total, rows); } private static async Task CheckOem(IFreeSql db, UserEntity user, CancellationToken ct) { if (!string.IsNullOrWhiteSpace(user.OemId) && !await db.Select().Where(o => o.OemId == user.OemId).AnyAsync(ct)) throw new AppException("OEM编号不存在"); } private static void Apply(UserEntity user, UserEditRequest request, UserEntity actor, bool subUser) { // 合法分组值已由服务参数检查保证,这里检查主/子账号身份是否匹配。 if (request.GroupId.HasValue && (subUser ? request.GroupId == 3 : request.GroupId != 3)) throw new AppException("主账号分组必须为3,子账号分组必须为4、5、8、9、10或11"); // 普通主账号只能授予自己已有的权限,不能通过创建子账号提升权限。 if (!IsAdmin(actor) && request.Authority != null) { var allowed = Split(actor.Authority); if (Split(request.Authority).Any(a => !allowed.Contains(a))) throw new AppException("不能授予超出自身范围的权限", 403); } if (!IsAdmin(actor) && (request.OemId != null || request.ShopPointsPerMonth != null || request.PostagePerOrder != null || request.RefundPostagePerOrder != null || request.GiftOemAmountPerOrder != null || request.SyncLogisticsPerOrder != null)) throw new AppException("仅管理员可以修改计费配置", 403); if (request.SaLevel.HasValue) { if (!IsAdmin(actor)) throw new AppException("无权设置管理员级别", 403); user.SaLevel = request.SaLevel; } user.NickName = request.NickName?.Trim() ?? user.NickName; user.Mobile = request.Mobile?.Trim() ?? user.Mobile; user.CompanyName = request.CompanyName?.Trim() ?? user.CompanyName; user.Notes = request.Notes ?? user.Notes; user.CardId = request.CardId ?? user.CardId; user.GroupId = request.GroupId ?? user.GroupId; user.Authority = request.Authority == null ? user.Authority : string.Join(",", Split(request.Authority)); user.ForbiddenPage = request.ForbiddenPage ?? user.ForbiddenPage; user.OemId = request.OemId?.Trim() ?? user.OemId; user.ShopPointsPerMonth = request.ShopPointsPerMonth ?? user.ShopPointsPerMonth; user.PostagePerOrder = request.PostagePerOrder ?? user.PostagePerOrder; user.RefundPostagePerOrder = request.RefundPostagePerOrder ?? user.RefundPostagePerOrder; user.GiftOemAmountPerOrder = request.GiftOemAmountPerOrder ?? user.GiftOemAmountPerOrder; user.SyncLogisticsPerOrder = request.SyncLogisticsPerOrder ?? user.SyncLogisticsPerOrder; } private static HashSet Split(string? value) => (value ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToHashSet(StringComparer.Ordinal); }