提交管理员操作一切

This commit is contained in:
chfei committed 2026-10-23 18:16:05 +08:00
1 parent a87ebeaffb
commit d6471f97d3
23 files changed
+600 -73

No files matched your search

@@ -0,0 +1,18 @@
using Store.Users.Entities;
namespace Store.Users.Services;
/// <summary>统一账户管理范围:管理员管理全部,主账号管理本人及直属子账号。</summary>
internal static class UserAccessPolicy
{
public static bool IsAdmin(UserEntity actor) => actor.SaLevel == 1;
public static bool Owns(UserEntity actor, UserEntity target) => actor.IsMainUser
&& string.Equals(target.ParentUserName, actor.UserName, StringComparison.OrdinalIgnoreCase);
public static bool CanRead(UserEntity actor, UserEntity target) =>
IsAdmin(actor) || actor.Id == target.Id || Owns(actor, target);
public static bool CanManage(UserEntity actor, UserEntity target) =>
IsAdmin(actor) || (!IsAdmin(target) && actor.IsMainUser && (actor.Id == target.Id || Owns(actor, target)));
}
@@ -37,6 +37,8 @@ public sealed class UserFundsService(IFreeSql orm, ICurrentUser currentUser) : I
var target = await db.Select<UserEntity>().Where(u => u.UserName == name).FirstAsync(ct)
?? throw new AppException("目标账户不存在");
if (transfer && actor.Id == target.Id) throw new AppException("不能向自己转移积分");
if (transfer && !UserAccessPolicy.CanManage(actor, target))
throw new AppException("只能向管理范围内的账户转移积分", 403);
// 双账户转移按相同主键顺序加锁,避免双方同时转账时产生反向锁顺序。
var ids = transfer ? new[] { actor.Id, target.Id }.Order().ToArray() : new[] { target.Id };
foreach (var id in ids)
@@ -49,6 +49,7 @@ internal static class UserInputChecks
if (request.Page is < 1 or > 1000000) throw new ValidationException("页码必须为1-1000000");
if (request.PageSize is < 1 or > 200) throw new ValidationException("每页条数必须为1-200");
Length(request.Keyword, 100, "查询关键字");
Length(request.ParentUserName, 20, "父账号");
Group(request.GroupId);
}
@@ -6,40 +6,57 @@ namespace Store.Users.Services;
public sealed partial class UserManagementService
{
/// <summary>以本人身份提交充值备注,不能伪造提交账户或昵称。</summary>
/// <summary>为本人或授权账户提交充值备注,昵称从目标账户读取。</summary>
public async Task<long> AddTopUpMessage(UserTopUpMessageRequest request, CancellationToken ct)
{
RequestValidator.Validate(request);
UserInputChecks.Length(request.Message, 1000, "充值备注");
UserInputChecks.Length(request.UserName, 20, "用户名");
var actor = await Actor(orm, ct);
var name = string.IsNullOrWhiteSpace(request.UserName) ? actor.UserName : request.UserName.Trim();
var target = await orm.Select<UserEntity>().Where(u => u.UserName == name).FirstAsync(ct)
?? throw new AppException("用户不存在");
if (!UserAccessPolicy.CanManage(actor, target) && actor.Id != target.Id)
throw new AppException("无权为该账户提交充值备注", 403);
return await orm.Insert(new UserTopUpMessageEntity
{
UserName = actor.UserName, NickName = actor.NickName, Message = request.Message.Trim()
UserName = target.UserName, NickName = target.NickName, Message = request.Message.Trim()
}).ExecuteIdentityAsync(ct);
}
/// <summary>管理员记录人工处理结果。备注处理与实际充值是不同用例。</summary>
/// <summary>管理范围内记录人工处理结果。备注处理不会自动增加积分或余额。</summary>
public async Task UpdateTopUpMessage(UserTopUpResultRequest request, CancellationToken ct)
{
RequestValidator.Validate(request);
UserInputChecks.PositiveId(request.Id);
UserInputChecks.Length(request.Result, 1000, "处理结果");
var actor = await Actor(orm, ct);
if (!IsAdmin(actor)) throw new AppException("仅管理员可以处理充值备注", 403);
if (!await orm.Select<UserTopUpMessageEntity>().Where(m => m.Id == request.Id).AnyAsync(ct))
throw new AppException("充值备注不存在");
var message = await orm.Select<UserTopUpMessageEntity>().Where(m => m.Id == request.Id).FirstAsync(ct)
?? throw new AppException("充值备注不存在");
if (!IsAdmin(actor))
{
var target = await orm.Select<UserEntity>().Where(u => u.UserName == message.UserName).FirstAsync(ct);
if (target == null || !UserAccessPolicy.CanManage(actor, target))
throw new AppException("无权处理该账户的充值备注", 403);
}
await orm.Update<UserTopUpMessageEntity>().Set(m => m.Result, request.Result.Trim())
.Where(m => m.Id == request.Id).ExecuteAffrowsAsync(ct);
}
/// <summary>普通用户仅查看自己提交的备注,管理员查看全部。</summary>
/// <summary>管理员查看全部,主账号查看本人和直属子账号,子账号仅查看本人备注。</summary>
public async Task<UserPage<UserTopUpMessageEntity>> TopUpMessages(UserSearchRequest request, CancellationToken ct)
{
RequestValidator.Validate(request);
UserInputChecks.Search(request);
var actor = await Actor(orm, ct);
var select = orm.Select<UserTopUpMessageEntity>();
if (!IsAdmin(actor)) select = select.Where(m => m.UserName == actor.UserName);
if (!IsAdmin(actor))
{
if (actor.IsMainUser)
select = select.Where(m => m.UserName == actor.UserName || orm.Select<UserEntity>()
.Where(u => u.UserName == m.UserName && u.ParentUserName == actor.UserName).Any());
else select = select.Where(m => m.UserName == actor.UserName);
}
var total = await select.CountAsync(ct);
var rows = await select.OrderByDescending(m => m.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
return new(total, rows);
@@ -23,10 +23,7 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
?? throw new UnauthorizedAccessException("登录账户不存在");
}
private static bool IsAdmin(UserEntity actor) => actor.SaLevel == 1;
private static bool Owns(UserEntity actor, UserEntity target) =>
actor.IsMainUser && string.Equals(target.ParentUserName, actor.UserName, StringComparison.OrdinalIgnoreCase);
private static bool IsAdmin(UserEntity actor) => UserAccessPolicy.IsAdmin(actor);
/// <summary>仅本人、直属主账号或超级管理员可以读取账户详情。</summary>
public async Task<UserInfo> GetUser(string? userName, CancellationToken ct)
{
@@ -36,21 +33,53 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
UserInputChecks.Length(name, 20, "用户名");
var target = await orm.Select<UserEntity>().Where(u => u.UserName == name).FirstAsync(ct)
?? throw new AppException("用户不存在");
if (!IsAdmin(actor) && actor.Id != target.Id && !Owns(actor, target))
if (!UserAccessPolicy.CanRead(actor, target))
throw new AppException("无权访问该账户", 403);
return await query.GetUser(target.UserName);
}
/// <summary>管理员查询全部账户;普通主账号只能查询自己的直属子账号。</summary>
/// <summary>解析代操作目标,服务端校验管理员或主子账号归属,省略用户名时操作本人。</summary>
public async Task<string> 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<UserEntity>().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;
}
/// <summary>管理员查询全部账户;普通主账号的列表范围为本人和直属子账号。</summary>
public async Task<UserPage<UserInfo>> Search(UserSearchRequest request, bool subUsersOnly, CancellationToken ct)
{
RequestValidator.Validate(request);
UserInputChecks.Search(request);
var actor = await Actor(orm, ct);
if (!subUsersOnly && !IsAdmin(actor)) throw new AppException("仅管理员可以查询全部账户", 403);
if (subUsersOnly && !actor.IsMainUser) throw new AppException("子账号不能管理其他账户", 403);
if (!subUsersOnly && !IsAdmin(actor) && !actor.IsMainUser) throw new AppException("无权查询账户列表", 403);
if (subUsersOnly && !IsAdmin(actor) && !actor.IsMainUser) throw new AppException("子账号不能管理其他账户", 403);
var select = orm.Select<UserEntity>();
if (subUsersOnly) select = select.Where(u => u.ParentUserName == actor.UserName);
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))
{
@@ -70,6 +99,7 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
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();
@@ -78,12 +108,23 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
// 锁住主账号,使其创建子账号和删除操作串行化。
actor = await db.Select<UserEntity>().Where(u => u.Id == actor.Id).ForUpdate().FirstAsync(ct)
?? throw new AppException("操作者账户不存在");
if (subUser ? !actor.IsMainUser : !IsAdmin(actor)) throw new AppException("无权创建该账户", 403);
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<UserEntity>().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<UserEntity>().Where(u => u.UserName == name).AnyAsync(ct)) throw new AppException("用户名已存在");
var user = new UserEntity
{
UserName = name, ParentUserName = subUser ? actor.UserName : "", GroupId = subUser ? (byte)4 : (byte)3,
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,
@@ -111,7 +152,8 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
var actor = await Actor(db, ct);
var target = await db.Select<UserEntity>().Where(u => u.Id == id).ForUpdate().FirstAsync(ct)
?? throw new AppException("用户不存在");
if (subUser ? !Owns(actor, target) : !IsAdmin(actor)) throw new AppException("无权修改该账户", 403);
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;
@@ -139,7 +181,7 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
var target = await db.Select<UserEntity>().Where(u => u.Id == id).ForUpdate().FirstAsync(ct)
?? throw new AppException("用户不存在");
if (actor.Id == target.Id) throw new AppException("不能删除当前登录账户");
if (!IsAdmin(actor) && !Owns(actor, target)) throw new AppException("无权删除该账户", 403);
if (!UserAccessPolicy.CanManage(actor, target)) throw new AppException("无权删除该账户", 403);
if ((target.Balance ?? 0) != 0 || target.Points != 0) throw new AppException("账户仍有余额或积分,不能删除");
if (await db.Select<UserEntity>().Where(u => u.ParentUserName == target.UserName).AnyAsync(ct))
throw new AppException("请先处理该账户的子账号");
@@ -152,19 +194,29 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
onlineUsers.SignOut(target.UserName);
}
/// <summary>比较并更新密码,防止并发密码变更丢失;成功后立即失效已有登录会话。</summary>
/// <summary>本人改密校验原密码,管理者可重置授权账户密码;比较并更新后失效目标账户会话。</summary>
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);
if (!UserPassword.Verify(actor, request.OldPassword)) throw new AppException("原密码错误");
var hash = UserPassword.Hash(actor, request.NewPassword);
var name = string.IsNullOrWhiteSpace(request.UserName) ? actor.UserName : request.UserName.Trim();
UserInputChecks.Length(name, 20, "用户名");
var target = await orm.Select<UserEntity>().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<UserEntity>().Set(u => u.Password, hash).Set(u => u.MdyTime, DateTime.Now)
.Where(u => u.Id == actor.Id && u.Password == actor.Password).ExecuteAffrowsAsync(ct) != 1)
.Where(u => u.Id == target.Id && u.Password == target.Password).ExecuteAffrowsAsync(ct) != 1)
throw new AppException("密码已发生变化,请重新登录");
onlineUsers.SignOut(actor.UserName);
onlineUsers.SignOut(target.UserName);
}
/// <summary>按账户归属查询积分流水;分页与账户详情使用相同的访问范围。</summary>
@@ -222,7 +274,7 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
throw new AppException("仅管理员可以修改计费配置", 403);
if (request.SaLevel.HasValue)
{
if (!IsAdmin(actor) || (subUser && request.SaLevel != 0)) throw new AppException("无权设置管理员级别", 403);
if (!IsAdmin(actor)) throw new AppException("无权设置管理员级别", 403);
user.SaLevel = request.SaLevel;
}
user.NickName = request.NickName?.Trim() ?? user.NickName;