using AdminService.User.Param; using AdminService.User.Response; using AdminService.GameConfigs; using Common; using Common.Utils; using CommonDomain; using CommonDomain.BaseModel; using CommonInfrastructure.Response; using DotNetCore.CAP; using Microsoft.Extensions.Logging; using MongoRepository; using RedisHelper; using RedisService; using UserDomain.Entity; using System.Linq.Expressions; using MongoDB.Driver; namespace AdminService.User { public class AdminUserService : IAdminUserService { private readonly IMongoBaseRepository _userMongo; private readonly IMongoBaseRepository _emailMongo; private readonly IMongoBaseRepository _rewardMongo; private readonly ILogger _logger; private readonly IRedisService _cache; private readonly IGameConfigService _gameConfigService; private readonly ICapPublisher _capPublisher; private readonly IDistributedLock _distributedLock; public AdminUserService( IMongoBaseRepository userMongo, IMongoBaseRepository emailMongo, IMongoBaseRepository rewardMongo, ILogger logger, IRedisService cache, IGameConfigService gameConfigService, ICapPublisher capPublisher, IDistributedLock distributedLock) { _userMongo = userMongo; _emailMongo = emailMongo; _rewardMongo = rewardMongo; _logger = logger; _cache = cache; _gameConfigService = gameConfigService; _capPublisher = capPublisher; _distributedLock = distributedLock; } public async Task<(bool isok, string msg)> CancelUser(LoginUser loginUser) { var user = await GetUser(loginUser.UserId); if (user == null) throw new BusinessException("用户不存在"); user.IsDelete = true; await _userMongo.ReplaceOneAsync(user); return (true, "注销成功"); } public async Task<(bool isok, string msg)> BanUser(string userId, string reason, int days) { var user = await _userMongo.FindByIdAsync(userId); if (user == null) throw new BusinessException("用户不存在"); user.BanInfo = new BanInfo(); user.BanInfo.IsBan = true; user.BanInfo.BanReason = reason; user.BanInfo.BanDays = days; user.BanInfo.BanTime = DateTime.Now.ToTimeStamp(); user.BanInfo.BanEndTime = days > 0 ? DateTime.Now.AddDays(days).ToTimeStamp() : 0; await _userMongo.ReplaceOneAsync(user); return (true, "封号成功"); } public async Task<(bool isok, string msg)> DeleteUser(string id) { return await _userMongo.DeleteByIdAsync(id); ; } public async Task<(bool isok, string msg)> BatchDeleteUser(List userIds) { if (userIds == null || userIds.Count == 0) return (false, "请选择用户"); var count = 0; foreach (var id in userIds) { var result = await _userMongo.DeleteByIdAsync(id); if (result.ok) count++; } return (true, $"已删除 {count} 个用户"); } public async Task<(bool isok, string msg)> UpdateUser(UserEntity user) { return await _userMongo.ReplaceOneAsync(user); } public async Task<(bool isok, string msg)> UpdateUser(UpdateUserDataParam param) { var user = await GetUser(param.Id); if (user == null) return (false, "用户不存在"); if (user.Assets == null) user.Assets = new Assets(); var beforeGold = user.Assets.Gold; var beforeDiamond = user.Assets.Diamond; var beforeLevel = user.Assets.Level; user.Assets.Diamond = param.Diamond; user.Assets.Gold = param.Gold; user.Assets.Level = param.Level; user.Assets.Exp = param.Exp; var result = await _userMongo.ReplaceOneAsync(user); if (result.ok) { PublishAssetChanges(user.Id, user.BaseInfo?.Number, beforeGold, user.Assets.Gold, beforeDiamond, user.Assets.Diamond, "后台修改", string.Empty); if (beforeLevel != user.Assets.Level) PublishLevelChange(user.Id, user.Assets.Level, user.Assets.Exp, user.Assets.Gold, user.Assets.Diamond, "后台修改等级"); } return result; } public async Task<(bool isok, string msg)> SendEmail(SendEmailParam param, LoginUser loginUser) { const int MAX_EMAIL_COUNT = 200; const int EMAIL_EXPIRE_DAYS = 30; var now = DateTime.Now.ToTimeStamp(); List ids = new List(); if (string.IsNullOrWhiteSpace(param.UserId)) { ids = (await _userMongo.FilterByAsync(c => (c.BanInfo == null || c.BanInfo.IsBan == false) && c.IsDelete == false)).Select(c => c.Id).ToList(); } else { ids = (await _userMongo.FilterByAsync(c => (c.BanInfo == null || c.BanInfo.IsBan == false) && c.IsDelete == false && param.UserId.Contains(c.BaseInfo.Number))).Select(c => c.Id).ToList(); } List emails = new List(); foreach (var id in ids) { var userEmails = await _emailMongo.FilterByAsync(c => c.UserId == id); if (userEmails != null && userEmails.Count >= MAX_EMAIL_COUNT) { var sorted = userEmails.OrderBy(c => c.SendTime).ToList(); var deleteCount = userEmails.Count - MAX_EMAIL_COUNT + 1; for (int i = 0; i < deleteCount; i++) { await _emailMongo.DeleteByIdAsync(sorted[i].Id); } } var expiredEmails = userEmails?.Where(c => c.ExpireTime > 0 && c.ExpireTime < now).ToList(); if (expiredEmails != null) { foreach (var exp in expiredEmails) { await _emailMongo.DeleteByIdAsync(exp.Id); } } var email = new UserEmail(); email.UserId = id; email.Title = param.Title; email.Content = param.Content; email.SendTime = DateTime.Now.ToTimeStamp(); email.RewardDiamond = param.RewardDiamond; email.RewardGold = param.RewardGold; email.RewardItems = param.RewardItems; email.IsReward = false; email.CreateInfo = new OptionUserInfo(loginUser.UserId, loginUser.UserName); email.ExpireTime = DateTime.Now.AddDays(EMAIL_EXPIRE_DAYS).ToTimeStamp(); emails.Add(email); } var res = await _emailMongo.InsertManyAsync(emails); foreach (var email in emails) { _capPublisher.Publish("BackgroundOperation.NotifyUser", new BackgroundOperationNotifyMessage { UserId = email.UserId, Action = "new_email", EmailId = email.Id, Title = email.Title }); } return (true, "发送成功"); } public async Task<(List list, int count)> GetAdminUserList(AdminUserListParam param) { var where = LinqExtensions.True().And(e => e.IsDelete == false); if (!string.IsNullOrWhiteSpace(param.KeyWord)) { where = where.And(e => e.BaseInfo.Number.Contains(param.KeyWord) || e.BaseInfo.UserName.Contains(param.KeyWord) || e.BaseInfo.Mobile.Contains(param.KeyWord)); } if (param.IsBan.HasValue) { if (param.IsBan.Value == 1) where = where.And(e => e.BanInfo != null && e.BanInfo.IsBan == true); else where = where.And(e => e.BanInfo == null || e.BanInfo.IsBan == false); } if (param.VipLevel.HasValue) { where = where.And(e => e.Assets.VipLevel == param.VipLevel.Value); } if (param.VipLevelMin.HasValue) { where = where.And(e => e.Assets.VipLevel >= param.VipLevelMin.Value); } if (param.VipLevelMax.HasValue) { where = where.And(e => e.Assets.VipLevel <= param.VipLevelMax.Value); } if (param.CreateTimeStart.HasValue) { where = where.And(e => e.CreateTime >= param.CreateTimeStart.Value); } if (param.CreateTimeEnd.HasValue) { where = where.And(e => e.CreateTime <= param.CreateTimeEnd.Value); } if (param.LastLoginTimeStart.HasValue) { where = where.And(e => e.Static.LastLoginTime >= param.LastLoginTimeStart.Value); } if (param.LastLoginTimeEnd.HasValue) { where = where.And(e => e.Static.LastLoginTime <= param.LastLoginTimeEnd.Value); } if (!string.IsNullOrWhiteSpace(param.Channel)) { where = where.And(e => e.BaseInfo.Channel == param.Channel); } if (!string.IsNullOrWhiteSpace(param.LoginIp)) { where = where.And(e => e.Static.LastLoginIp.Contains(param.LoginIp)); } if (param.UserType.HasValue) { where = where.And(e => e.UserType == param.UserType.Value); } HashSet? onlineLoginIds = null; if (param.IsOnline.HasValue) { onlineLoginIds = _cache.GetAllLoginUserIds().ToHashSet(StringComparer.Ordinal); } var orderBy = param.OrderBy?.ToLower() ?? "createtime"; var orderDesc = true; if (orderBy == "totalgold") { var allUsers = await _userMongo.FilterByAsync(where); if (onlineLoginIds != null) allUsers = allUsers.Where(u => onlineLoginIds.Contains(u.Id)).ToList(); var totalGoldUsers = allUsers.Select(u => new { User = u, TotalGold = (u.Assets?.Gold ?? 0) + (u.SafeBox?.Amount ?? 0) }).OrderByDescending(x => x.TotalGold).ToList(); var totalCount = totalGoldUsers.Count; var pagedUsers = totalGoldUsers .Skip(((param.PageIndex ?? 1) - 1) * (param.PageSize ?? 10)) .Take(param.PageSize ?? 10) .Select(x => x.User) .ToList(); return (pagedUsers, totalCount); } Expression> orderSelector = c => c.CreateTime; switch (orderBy) { case "lastlogintime": orderSelector = c => c.Static.LastLoginTime; break; case "diamond": orderSelector = c => c.Assets.Diamond; break; case "gold": orderSelector = c => c.Assets.Gold; break; case "createtime": default: orderSelector = c => c.CreateTime; break; } if (onlineLoginIds != null) { var allUsers = await _userMongo.FilterByAsync(where); var filtered = allUsers.Where(u => onlineLoginIds.Contains(u.Id)).ToList(); var ranked = orderBy switch { "lastlogintime" => filtered.OrderByDescending(u => u.Static.LastLoginTime), "diamond" => filtered.OrderByDescending(u => u.Assets.Diamond), "gold" => filtered.OrderByDescending(u => u.Assets.Gold), _ => filtered.OrderByDescending(u => u.CreateTime) }; var list = ranked .Skip(((param.PageIndex ?? 1) - 1) * (param.PageSize ?? 10)) .Take(param.PageSize ?? 10) .ToList(); return (list, list.Count == 0 ? 0 : list.Count); } var result = await _userMongo.QueryPageAsync(where, orderSelector, orderDesc, param.PageSize ?? 10, param.PageIndex ?? 1); return (result.list, (int)result.count); } public async Task<(bool isok, string msg)> UpdateUserAssets(UpdateUserAssetsParam param) { var lockKey = $"user:asset:update:{param.UserId}"; var result = await _distributedLock.TryExecuteAsync(lockKey, async () => { var user = await GetUser(param.UserId); if (user == null) return (false, "用户不存在"); var beforeGold = user.Assets.Gold; var beforeDiamond = user.Assets.Diamond; var beforeLevel = user.Assets.Level; switch (param.OperationType) { case 1: if (param.Diamond != 0) { var diamondReduce = param.Diamond < 0; user.Assets.Diamond += param.Diamond; await _rewardMongo.InsertOneAsync(new UserReward { UserId = user.Id, BeforeCount = user.Assets.Diamond - param.Diamond, AfterCount = user.Assets.Diamond, ChangeCount = diamondReduce ? -param.Diamond : param.Diamond, CurrencyType = 3, Type = diamondReduce ? 2 : 1, Remark = $"管理员{(diamondReduce ? "减少" : "增加")}钻石 - {param.Remark}" }); } if (param.Gold != 0) { var goldReduce = param.Gold < 0; user.Assets.Gold += param.Gold; await _rewardMongo.InsertOneAsync(new UserReward { UserId = user.Id, BeforeCount = user.Assets.Gold - param.Gold, AfterCount = user.Assets.Gold, ChangeCount = goldReduce ? -param.Gold : param.Gold, CurrencyType = 1, Type = goldReduce ? 2 : 1, Remark = $"管理员{(goldReduce ? "减少" : "增加")}金币 - {param.Remark}" }); } if (param.SafeBoxGold != 0) { user.SafeBox ??= new SafeBoxInfo(); var safeBoxReduce = param.SafeBoxGold < 0; user.SafeBox.Amount += param.SafeBoxGold; if (user.SafeBox.Amount < 0) return (false, "保险箱金币数量不足"); await _rewardMongo.InsertOneAsync(new UserReward { UserId = user.Id, BeforeCount = user.SafeBox.Amount - param.SafeBoxGold, AfterCount = user.SafeBox.Amount, ChangeCount = safeBoxReduce ? -param.SafeBoxGold : param.SafeBoxGold, CurrencyType = 2, Type = safeBoxReduce ? 2 : 1, Remark = $"管理员{(safeBoxReduce ? "减少" : "增加")}保险箱金币 - {param.Remark}" }); } if (param.Exp.HasValue) { user.Assets.Exp += param.Exp.Value; } break; case 2: if (param.Diamond != 0) { if (user.Assets.Diamond < param.Diamond) return (false, "钻石数量不足"); user.Assets.Diamond -= param.Diamond; await _rewardMongo.InsertOneAsync(new UserReward { UserId = user.Id, BeforeCount = user.Assets.Diamond + param.Diamond, AfterCount = user.Assets.Diamond, ChangeCount = param.Diamond, CurrencyType = 3, Type = 2, Remark = $"管理员减少钻石 - {param.Remark}" }); } if (param.Gold != 0) { if (user.Assets.Gold < param.Gold) return (false, "金币数量不足"); user.Assets.Gold -= param.Gold; await _rewardMongo.InsertOneAsync(new UserReward { UserId = user.Id, BeforeCount = user.Assets.Gold + param.Gold, AfterCount = user.Assets.Gold, ChangeCount = param.Gold, CurrencyType = 1, Type = 2, Remark = $"管理员减少金币 - {param.Remark}" }); } if (param.Exp.HasValue && param.Exp.Value > 0) { if (user.Assets.Exp < param.Exp.Value) return (false, "经验值不足"); user.Assets.Exp -= param.Exp.Value; } break; case 3: if (param.Diamond != 0) { await _rewardMongo.InsertOneAsync(new UserReward { UserId = user.Id, BeforeCount = user.Assets.Diamond, AfterCount = param.Diamond, ChangeCount = param.Diamond - user.Assets.Diamond, CurrencyType = 3, Type = param.Diamond >= user.Assets.Diamond ? 1 : 2, Remark = $"管理员设置钻石 - {param.Remark}" }); user.Assets.Diamond = param.Diamond; } if (param.Gold != 0) { await _rewardMongo.InsertOneAsync(new UserReward { UserId = user.Id, BeforeCount = user.Assets.Gold, AfterCount = param.Gold, ChangeCount = param.Gold - user.Assets.Gold, CurrencyType = 1, Type = param.Gold >= user.Assets.Gold ? 1 : 2, Remark = $"管理员设置金币 - {param.Remark}" }); user.Assets.Gold = param.Gold; } if (param.SafeBoxGold != 0) { user.SafeBox ??= new SafeBoxInfo(); var beforeSafeBox = user.SafeBox.Amount; if (param.SafeBoxGold < 0) return (false, "保险箱金币数量不足"); await _rewardMongo.InsertOneAsync(new UserReward { UserId = user.Id, BeforeCount = beforeSafeBox, AfterCount = param.SafeBoxGold, ChangeCount = Math.Abs(param.SafeBoxGold - beforeSafeBox), CurrencyType = 2, Type = param.SafeBoxGold >= beforeSafeBox ? 1 : 2, Remark = $"管理员设置保险箱金币 - {param.Remark}" }); user.SafeBox.Amount = param.SafeBoxGold; } if (param.Exp.HasValue) { user.Assets.Exp = param.Exp.Value; } if (param.Level.HasValue) { user.Assets.Level = param.Level.Value; } if (param.VipLevel.HasValue) { user.Assets.VipLevel = param.VipLevel.Value; if (param.VipLevel.Value > 0) { var gameConfig = await _gameConfigService.Get(); var vipConfig = gameConfig?.VipLevelConfigs? .FirstOrDefault(c => c.Level == param.VipLevel.Value); if (vipConfig != null) { user.Assets.VipExpExtra += vipConfig.ExpMin; user.Assets.VipExp = (long)(user.Static?.TotalRecharge ?? 0) * 10 + user.Assets.VipExpExtra; user.Assets.VipSignInAdd = vipConfig.SignInAdd; } } } break; } var updateDefinition = Builders.Update .Set(p => p.Assets, user.Assets) .Set(p => p.SafeBox, user.SafeBox); var updateResult = await _userMongo.UpdateByIdAsync(user.Id, updateDefinition); if (!updateResult.ok) { return (false, updateResult.message); } PublishAssetChanges(user.Id, user.BaseInfo?.Number, beforeGold, user.Assets.Gold, beforeDiamond, user.Assets.Diamond, "后台修改", param.Remark); if (beforeLevel != user.Assets.Level) PublishLevelChange(user.Id, user.Assets.Level, user.Assets.Exp, user.Assets.Gold, user.Assets.Diamond, "后台修改等级"); return (true, "修改成功"); }, expiry: 10, retry: false); return result; } private void PublishAssetChanges(string userId, string userNumber, long beforeGold, long newGold, int beforeDiamond, int newDiamond, string source, string remark) { if (beforeGold != newGold) PublishAssetChange(userId, userNumber, 1, beforeGold, newGold, newGold, newDiamond, source, remark); if (beforeDiamond != newDiamond) PublishAssetChange(userId, userNumber, 2, beforeDiamond, newDiamond, newGold, newDiamond, source, remark); } private void PublishAssetChange(string userId, string userNumber, int currencyType, long beforeValue, long newValue, long currentGold, int currentDiamond, string source, string remark) { var changeType = newValue >= beforeValue ? 1 : 2; var amount = Math.Abs(newValue - beforeValue); try { _capPublisher.Publish("BackgroundOperation.NotifyUser", new BackgroundOperationNotifyMessage { UserId = userId, UserNumber = userNumber ?? string.Empty, CurrencyType = currencyType, ChangeType = changeType, Amount = amount, NewGold = currentGold, NewDiamond = currentDiamond, Source = source, Remark = remark ?? string.Empty }); } catch (Exception ex) { _logger.LogError(ex, "后台资产变更通知发布失败 userId={UserId} currencyType={CurrencyType} newValue={NewValue}", userId, currencyType, newValue); } } private void PublishLevelChange(string userId, int newLevel, long newExp, long currentGold, int currentDiamond, string remark) { try { _capPublisher.Publish("BackgroundOperation.NotifyUser", new BackgroundOperationNotifyMessage { UserId = userId, Action = "level_change", NewLevel = newLevel, NewExp = newExp, NewGold = currentGold, NewDiamond = currentDiamond, Source = remark, Remark = remark }); } catch (Exception ex) { _logger.LogError(ex, "后台等级变更通知发布失败 userId={UserId} newLevel={NewLevel}", userId, newLevel); } } public async Task GetUserStatistics() { var allUsers = await _userMongo.FilterByAsync(u => u.IsDelete == false); var now = DateTime.Now; var todayStart = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0).ToTimeStamp(); var monthStart = new DateTime(now.Year, now.Month, 1, 0, 0, 0).ToTimeStamp(); var statistics = new UserStatistics { TotalUsers = allUsers.Count(), TodayRegisteredUsers = allUsers.Count(u => u.CreateTime >= todayStart), MonthRegisteredUsers = allUsers.Count(u => u.CreateTime >= monthStart), BannedUsers = allUsers.Count(u => u.BanInfo != null && u.BanInfo.IsBan == true), VipUsers = allUsers.Count(u => u.Assets.VipLevel > 0), TotalGold = allUsers.Sum(u => (long)u.Assets.Gold), TotalDiamond = allUsers.Sum(u => (long)u.Assets.Diamond) }; statistics.VipLevelCount = allUsers .GroupBy(u => u.Assets.VipLevel) .ToDictionary(g => g.Key, g => (long)g.Count()); statistics.OnlineUsers = _cache.GetAllLoginUserIds().Count(); return statistics; } public async Task GetEmailStatistics() { var now = DateTime.Now; var todayStart = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0).ToTimeStamp(); var monthStart = new DateTime(now.Year, now.Month, 1, 0, 0, 0).ToTimeStamp(); var allEmails = await _emailMongo.FilterByAsync(e => true); return new EmailStatistics { TotalEmails = allEmails.LongCount(), TodayEmails = allEmails.LongCount(e => e.SendTime >= todayStart), MonthEmails = allEmails.LongCount(e => e.SendTime >= monthStart), RewardEmails = allEmails.LongCount(e => e.IsReward == false), UnclaimedEmails = allEmails.LongCount(e => e.IsReward == true) }; } public async Task<(List list, long count)> GetEmailList(EmailListQueryParam param) { var keyword = param.UserNumber?.Trim(); List matchedUserIds = null; if (!string.IsNullOrWhiteSpace(keyword)) { var matchedUsers = await _userMongo.FilterByAsync(u => (u.BaseInfo.Number != null && u.BaseInfo.Number.Contains(keyword)) || (u.BaseInfo.UserName != null && u.BaseInfo.UserName.Contains(keyword)) || u.Id.Contains(keyword)); matchedUserIds = matchedUsers?.Select(u => u.Id).ToList() ?? new List(); } var result = await _emailMongo.QueryPageAsync( filterExpression: e => (matchedUserIds == null || matchedUserIds.Contains(e.UserId) || (!string.IsNullOrWhiteSpace(keyword) && e.Title != null && e.Title.Contains(keyword)) || (!string.IsNullOrWhiteSpace(keyword) && e.Content != null && e.Content.Contains(keyword))) && (string.IsNullOrWhiteSpace(param.UserId) || e.UserId == param.UserId) && (!param.ReadStatus.HasValue || (param.ReadStatus == 1 ? e.IsRead : !e.IsRead)) && (!param.RewardStatus.HasValue || (param.RewardStatus == 1 ? e.IsReward : !e.IsReward)) && (!param.StartTime.HasValue || e.SendTime >= param.StartTime) && (!param.EndTime.HasValue || e.SendTime <= param.EndTime), orderExpression: e => e.SendTime, desc: true, pageSize: param.PageSize ?? 10, pageIndex: param.PageIndex ?? 1 ); return (result.list.ToList(), result.count); } public async Task> GetEmailUserNamesAsync(List userIds) { var result = new Dictionary(); if (userIds == null || userIds.Count == 0) return result; var users = await _userMongo.FilterByAsync(u => userIds.Contains(u.Id)); foreach (var u in users) { if (u == null || string.IsNullOrWhiteSpace(u.Id)) continue; result[u.Id] = (u.BaseInfo?.UserName ?? "", u.BaseInfo?.Number ?? ""); } return result; } public async Task<(bool ok, string message)> DeleteEmail(string id) { return await _emailMongo.DeleteByIdAsync(id); } public async Task<(int success, int fail)> BatchDeleteEmail(List ids) { int success = 0, fail = 0; foreach (var id in ids) { var result = await _emailMongo.DeleteByIdAsync(id); if (result.ok) success++; else fail++; } return (success, fail); } public async Task<(bool isok, string msg)> BatchBanUser(BatchBanUserParam param) { var users = await _userMongo.FilterByAsync(_ => param.UserIds.Contains(_.Id)); var now = DateTime.Now.ToTimeStamp(); foreach (var user in users) { if (user.BanInfo == null) user.BanInfo = new BanInfo(); user.BanInfo.IsBan = true; user.BanInfo.BanTime = now; user.BanInfo.BanReason = param.Reason; user.BanInfo.BanDays = param.Days; if (param.Days > 0) { user.BanInfo.BanEndTime = now + (long)TimeSpan.FromDays(param.Days).TotalSeconds; } else { user.BanInfo.BanEndTime = 0; } await _userMongo.ReplaceOneAsync(user); } return (true, $"已封号 {users.Count()} 个用户"); } public async Task<(bool isok, string msg)> UnbanUser(string userId) { var user = await _userMongo.FindByIdAsync(userId); if (user == null) return (false, "用户不存在"); if (user.BanInfo == null || !user.BanInfo.IsBan) return (false, "用户未被封号"); user.BanInfo.IsBan = false; await _userMongo.ReplaceOneAsync(user); return (true, "解封成功"); } public async Task<(bool isok, string msg)> BatchSendEmail(BatchSendEmailParam param) { const int MAX_EMAIL_COUNT = 200; var now = DateTime.Now.ToTimeStamp(); var expireDays = param.ExpireDays > 0 ? param.ExpireDays : 30; var expireTime = DateTime.Now.AddDays(expireDays).ToTimeStamp(); List userIds; if (param.UserIds == null || param.UserIds.Count == 0) { var users = await _userMongo.FilterByAsync(u => u.IsDelete == false && (u.BanInfo == null || u.BanInfo.IsBan == false)); userIds = users.Select(u => u.Id).ToList(); } else { var users = await _userMongo.FilterByAsync(u => param.UserIds.Contains(u.BaseInfo.Number)); userIds = users.Select(u => u.Id).ToList(); } var emails = new List(); foreach (var userId in userIds) { var userEmails = await _emailMongo.FilterByAsync(c => c.UserId == userId); if (userEmails != null && userEmails.Count >= MAX_EMAIL_COUNT) { var sorted = userEmails.OrderBy(c => c.SendTime).ToList(); var deleteCount = userEmails.Count - MAX_EMAIL_COUNT + 1; for (int i = 0; i < deleteCount; i++) { await _emailMongo.DeleteByIdAsync(sorted[i].Id); } } var expiredEmails = userEmails?.Where(c => c.ExpireTime > 0 && c.ExpireTime < now).ToList(); if (expiredEmails != null) { foreach (var exp in expiredEmails) { await _emailMongo.DeleteByIdAsync(exp.Id); } } var email = new UserEmail { UserId = userId, Title = param.Title, Content = param.Content, RewardGold = param.Gold, RewardDiamond = param.Diamond, RewardItems = param.Props, SendTime = DateTime.Now.ToTimeStamp(), ExpireTime = expireTime, IsRead = false, IsReward = !(param.Gold > 0 || param.Diamond > 0 || (param.Props != null && param.Props.Count > 0)) }; emails.Add(email); } await _emailMongo.InsertManyAsync(emails); foreach (var email in emails) { _capPublisher.Publish("BackgroundOperation.NotifyUser", new BackgroundOperationNotifyMessage { UserId = email.UserId, Action = "new_email", EmailId = email.Id, Title = email.Title }); } return (true, $"已发送 {emails.Count} 封邮件"); } public async Task GetUser(string id) { var model = await _userMongo.FindOneAsync(c => c.Id == id || c.BaseInfo.Number == id); if (model == null) return null; if (model.BanInfo != null && model.BanInfo.IsBan) { if (DateTime.Now.ToTimeStamp() > model.BanInfo.BanEndTime) { model.BanInfo.IsBan = false; model.BanInfo.BanDays = 0; model.BanInfo.BanEndTime = 0; model.BanInfo.BanReason = ""; model.BanInfo.BanTime = 0; await _userMongo.ReplaceOneAsync(model); return model; } throw new BusinessException("用户已被封禁 原因:" + model.BanInfo.BanReason + " 到期时间" + model.BanInfo.BanEndTime.ToDateTime().ToString("yyyy-MM-dd hh:mm")); } if (model.IsDelete) throw new BusinessException("用户已被删除"); return model; } public async Task<(bool isok, string msg)> SetUserType(string userId, int userType) { var user = await _userMongo.FindByIdAsync(userId); if (user == null) return (false, "用户不存在"); if (userType < 1 || userType > 3) return (false, "用户类型无效,只能为1(普通用户)、2(代理商)、3(内部人员)"); user.UserType = userType; await _userMongo.ReplaceOneAsync(user); return (true, "设置成功"); } public async Task<(bool isok, string msg)> BatchSetUserType(List userIds, int userType) { if (userIds == null || userIds.Count == 0) return (false, "请选择用户"); if (userType < 1 || userType > 3) return (false, "用户类型无效,只能为1(普通用户)、2(代理商)、3(内部人员)"); var count = 0; foreach (var userId in userIds) { var user = await _userMongo.FindByIdAsync(userId); if (user != null) { user.UserType = userType; await _userMongo.ReplaceOneAsync(user); count++; } } return (true, $"已设置 {count} 个用户的类型"); } public async Task<(bool isok, string msg)> SetBlackUser(string userId, bool isBlack) { var operationId = Guid.NewGuid().ToString("N"); var startedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var user = await _userMongo.FindByIdAsync(userId); if (user == null) return (false, "用户不存在"); user.PlayerFactor ??= new PlayerFactor(); user.PlayerFactor.IsBlack = isBlack; if (isBlack) { var gameConfig = await _gameConfigService.Get(); user.PlayerFactor.ViolateValue = 0.01; user.PlayerFactor.CompensateRate = new CompensateRateParam { BossRate = 0.01, GoldRate = 0.01, SpecialRate = 0.01, Ordinary = 0.01 }; user.PlayerFactor.PenaltyRate = new CompensateRateParam { BossRate = 0.01, GoldRate = 0.01, SpecialRate = 0.01, Ordinary = 0.01 }; } else { var gameConfig = await _gameConfigService.Get(); user.PlayerFactor.ViolateValue = gameConfig.KillConfig.ViolateValue; user.PlayerFactor.CompensateRate = gameConfig.KillConfig.CompensateRate; user.PlayerFactor.PenaltyRate = gameConfig.KillConfig.PenaltyRate; user.PlayerFactor.CompensateQuota = 0; user.PlayerFactor.PenaltyQuota = 0; } var saveResult = await _userMongo.ReplaceOneAsync(user); if (!saveResult.ok) { return (false, saveResult.message); } if (!PublishFactorChanged(userId, operationId, isBlack ? "AddBlacklist" : "RemoveBlacklist", startedAt)) return (false, $"数据已保存,但刷新通知发布失败,操作编号:{operationId}"); return (true, isBlack ? "已屏蔽用户" : "已取消屏蔽"); } public async Task<(bool isok, string msg)> BatchSetBlackUser(List userIds, bool isBlack) { if (userIds == null || userIds.Count == 0) return (false, "请选择用户"); var count = 0; var failedCount = 0; foreach (var userId in userIds) { var operationId = Guid.NewGuid().ToString("N"); var startedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var user = await _userMongo.FindByIdAsync(userId); if (user != null) { user.PlayerFactor ??= new PlayerFactor(); user.PlayerFactor.IsBlack = isBlack; if (isBlack) { user.PlayerFactor.ViolateValue = 0.01; user.PlayerFactor.CompensateRate = new CompensateRateParam { BossRate = 0.01, GoldRate = 0.01, SpecialRate = 0.01, Ordinary = 0.01 }; user.PlayerFactor.PenaltyRate = new CompensateRateParam { BossRate = 0.01, GoldRate = 0.01, SpecialRate = 0.01, Ordinary = 0.01 }; } else { var gameConfig = await _gameConfigService.Get(); user.PlayerFactor.ViolateValue = gameConfig.KillConfig.ViolateValue; user.PlayerFactor.CompensateRate = gameConfig.KillConfig.CompensateRate; user.PlayerFactor.PenaltyRate = gameConfig.KillConfig.PenaltyRate; user.PlayerFactor.CompensateQuota = 0; user.PlayerFactor.PenaltyQuota = 0; } var saveResult = await _userMongo.ReplaceOneAsync(user); if (saveResult.ok) { if (PublishFactorChanged(userId, operationId, isBlack ? "BatchAddBlacklist" : "BatchRemoveBlacklist", startedAt)) count++; else failedCount++; } else { failedCount++; } } } return (failedCount == 0, isBlack ? $"已屏蔽 {count} 个用户,失败 {failedCount} 个" : $"已取消屏蔽 {count} 个用户,失败 {failedCount} 个"); } public async Task<(bool isok, string msg)> UpdateUserFactor(string userId, PlayerFactor factor) { var operationId = Guid.NewGuid().ToString("N"); var startedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var user = await _userMongo.FindByIdAsync(userId); if (user == null) return (false, "用户不存在"); if (user.PlayerFactor == null) user.PlayerFactor = new PlayerFactor(); var existing = user.PlayerFactor; existing.ViolateValue = factor.ViolateValue; existing.MissRate = factor.MissRate; if (factor.CompensateQuota > 0) { var currentGold = user.Assets?.Gold ?? 0; var session = _cache.GetRedisUserCId(userId); if (!string.IsNullOrWhiteSpace(session?.RoomId)) { var room = _cache.GetGameRoom(session.RoomId); var onlinePlayer = room?.Players?.FirstOrDefault(p => p.UserId == userId); if (onlinePlayer != null) currentGold = onlinePlayer.Gold; } existing.CompensateStartGold = currentGold; existing.CompensateTargetGold = checked(currentGold + factor.CompensateQuota); } else { existing.CompensateStartGold = 0; existing.CompensateTargetGold = 0; } existing.CompensateQuota = factor.CompensateQuota; existing.PenaltyQuota = factor.PenaltyQuota; existing.CompensateRate = factor.CompensateRate; existing.PenaltyRate = factor.PenaltyRate; existing.AdditionalFactor = factor.AdditionalFactor; existing.IsBlack = existing.IsBlack; existing.IsIgnore = existing.IsIgnore; existing.StartingBonus = existing.StartingBonus; var saveResult = await _userMongo.ReplaceOneAsync(user); if (!saveResult.ok) { return (false, saveResult.message); } try { var publishedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); _capPublisher.Publish(TopicKeys.因子变更, new PlayerFactorChangedMessage { UserId = userId, ChangeType = "UpdateFactor", OperationId = operationId, PublishedAtUnixMs = publishedAt }); } catch (Exception ex) { _logger.LogError(ex, "[FactorChange] CAP发布失败 operationId={OperationId} topic={Topic} userId={UserId}", operationId, TopicKeys.因子变更, userId); return (false, $"数据已保存,但刷新通知发布失败,操作编号:{operationId}"); } return (true, "更新成功"); } public async Task GetBlackFactorConfig() { return new PlayerFactor { IsBlack = true, ViolateValue = 0.01, CompensateRate = new CompensateRateParam { BossRate = 0.01, GoldRate = 0.01, SpecialRate = 0.01, Ordinary = 0.01 }, PenaltyRate = new CompensateRateParam { BossRate = 0.01, GoldRate = 0.01, SpecialRate = 0.01, Ordinary = 0.01 }, CompensateQuota = 0, PenaltyQuota = 0 }; } public async Task<(bool isok, string msg)> ResetUserFactor(string userId) { var operationId = Guid.NewGuid().ToString("N"); var startedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var user = await _userMongo.FindByIdAsync(userId); if (user == null) return (false, "用户不存在"); var gameConfig = await _gameConfigService.Get(); user.PlayerFactor = new PlayerFactor { IsBlack = false, ViolateValue = gameConfig.KillConfig.ViolateValue, CompensateRate = gameConfig.KillConfig.CompensateRate, PenaltyRate = gameConfig.KillConfig.PenaltyRate, CompensateQuota = 0, PenaltyQuota = 0 }; var saveResult = await _userMongo.ReplaceOneAsync(user); if (!saveResult.ok) { return (false, saveResult.message); } if (!PublishFactorChanged(userId, operationId, "ResetFactor", startedAt)) return (false, $"数据已保存,但刷新通知发布失败,操作编号:{operationId}"); return (true, "重置成功"); } private bool PublishFactorChanged(string userId, string operationId, string changeType, long startedAt) { try { var publishedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); _capPublisher.Publish(TopicKeys.因子变更, new PlayerFactorChangedMessage { UserId = userId, ChangeType = changeType, OperationId = operationId, PublishedAtUnixMs = publishedAt }); return true; } catch (Exception ex) { _logger.LogError(ex, "[FactorChange] CAP发布失败 operationId={OperationId} changeType={ChangeType} topic={Topic} userId={UserId}", operationId, changeType, TopicKeys.因子变更, userId); return false; } } public async Task<(bool isok, string msg)> InitUserPassword(string userId) { if (string.IsNullOrWhiteSpace(userId)) return (false, "用户不存在"); userId = userId.Trim(); var user = await GetUser(userId); if (user == null) { return (false, "用户不存在"); } user.BaseInfo.Password = Md5Helper.GetMD5("123456"); await _userMongo.ReplaceOneAsync(user); return (true, "用户密码已初始化为123456"); } public async Task<(bool isok, string msg)> InitSafeBoxPassword(string userId) { if (string.IsNullOrWhiteSpace(userId)) return (false, "用户不存在"); userId = userId.Trim(); var user = await GetUser(userId); if (user == null) { return (false, "用户不存在"); } user.SafeBox ??= new SafeBoxInfo(); user.SafeBox.Password = Common.Utils.Md5Helper.GetMD5("123456"); await _userMongo.ReplaceOneAsync(user); return (true, "保险箱密码已初始化为123456"); } public async Task<(List list, int count)> GetUserFactorList(UserFactorListParam param) { var where = LinqExtensions.True().And(e => e.IsDelete == false); if (!string.IsNullOrWhiteSpace(param.KeyWord)) { where = where.And(e => (e.BaseInfo != null && (e.BaseInfo.UserName.Contains(param.KeyWord) || e.BaseInfo.Number.Contains(param.KeyWord) || e.BaseInfo.Mobile.Contains(param.KeyWord)))); } if (!string.IsNullOrWhiteSpace(param.RegisterIp)) { where = where.And(e => e.RegisterIp != null && e.RegisterIp.Contains(param.RegisterIp)); } if (!string.IsNullOrWhiteSpace(param.LoginIp)) { where = where.And(e => e.Static != null && e.Static.LastLoginIp != null && e.Static.LastLoginIp.Contains(param.LoginIp)); } if (param.IsBlack.HasValue) { if (param.IsBlack.Value) { where = where.And(e => e.PlayerFactor != null && e.PlayerFactor.IsBlack); } else { where = where.And(e => e.PlayerFactor == null || !e.PlayerFactor.IsBlack); } } HashSet? onlineLoginIds = null; if (param.IsOnline.HasValue) { onlineLoginIds = _cache.GetAllLoginUserIds().ToHashSet(StringComparer.Ordinal); } List resultList; long totalCount; if (onlineLoginIds != null) { var allUsers = await _userMongo.FilterByAsync(where); IEnumerable filtered = allUsers.Where(u => onlineLoginIds.Contains(u.Id)); if (!string.IsNullOrWhiteSpace(param.IpCountOp) && param.IpCount.HasValue) { var onlineIpGroups = filtered .Where(u => !string.IsNullOrEmpty(u.RegisterIp)) .GroupBy(u => u.RegisterIp) .Where(g => { var count = g.Count(); return param.IpCountOp switch { "gt" => count > param.IpCount.Value, "gte" => count >= param.IpCount.Value, "lt" => count < param.IpCount.Value, "lte" => count <= param.IpCount.Value, "eq" => count == param.IpCount.Value, _ => true }; }) .SelectMany(g => g) .Select(u => u.Id) .ToHashSet(); filtered = filtered.Where(u => onlineIpGroups.Contains(u.Id)); } var ranked = filtered.OrderByDescending(u => u.CreateTime); resultList = ranked .Skip(((param.PageIndex ?? 1) - 1) * (param.PageSize ?? 15)) .Take(param.PageSize ?? 15) .ToList(); totalCount = ranked.Count(); } else if (!string.IsNullOrWhiteSpace(param.IpCountOp) && param.IpCount.HasValue) { var allUsers = await _userMongo.FilterByAsync(where); var ipGroups = allUsers .Where(u => !string.IsNullOrEmpty(u.RegisterIp)) .GroupBy(u => u.RegisterIp) .Where(g => { var count = g.Count(); return param.IpCountOp switch { "gt" => count > param.IpCount.Value, "gte" => count >= param.IpCount.Value, "lt" => count < param.IpCount.Value, "lte" => count <= param.IpCount.Value, "eq" => count == param.IpCount.Value, _ => true }; }) .SelectMany(g => g) .Select(u => u.Id) .ToHashSet(); where = where.And(e => ipGroups.Contains(e.Id)); var filteredResult = await _userMongo.QueryPageAsync(where, c => c.CreateTime, true, param.PageSize ?? 15, param.PageIndex ?? 1); resultList = filteredResult.list; totalCount = filteredResult.count; } else { var result = await _userMongo.QueryPageAsync(where, c => c.CreateTime, true, param.PageSize ?? 15, param.PageIndex ?? 1); resultList = result.list; totalCount = result.count; } return (resultList, (int)totalCount); } public async Task> GetUsersByRegisterIps(List registerIps) { var filter = Builders.Filter.In(u => u.RegisterIp, registerIps); return await _userMongo.FilterByAsync(filter); } public async Task> GetUserProps(string userId) { var user = await _userMongo.FindByIdAsync(userId); return user?.Props ?? new List(); } public async Task<(bool isok, string msg)> AddUserProp(string userId, string propNumber, int count) { var user = await _userMongo.FindByIdAsync(userId); if (user == null) return (false, "用户不存在"); if (count <= 0) return (false, "数量必须大于0"); var prop = user.Props.FirstOrDefault(p => p.Number == propNumber); if (prop != null) { prop.Count += count; } else { var gameConfig = _cache.GetGameConfig(); var propConfig = gameConfig.PropConfig.FirstOrDefault(c => c.Number == propNumber); if (propConfig == null) return (false, "道具配置不存在"); user.Props.Add(new BaseProductCount { Number = propConfig.Number, Name = propConfig.Name, Type = propConfig.Type, ImageUrl = propConfig.ImageUrl, Count = count }); } var update = Builders.Update.Set(p => p.Props, user.Props); var (ok, message) = await _userMongo.UpdateByIdAsync(user.Id, update); if (ok) { _capPublisher.Publish("BackgroundOperation.NotifyUser", new BackgroundOperationNotifyMessage { UserId = userId, Action = "backpack_add", PropNumber = propNumber, Count = count }); } return (ok, message); } public async Task<(bool isok, string msg)> UpdateUserPropCount(string userId, string propNumber, int count) { var user = await _userMongo.FindByIdAsync(userId); if (user == null) return (false, "用户不存在"); var prop = user.Props.FirstOrDefault(p => p.Number == propNumber); if (prop == null) return (false, "道具不存在"); if (count <= 0) { user.Props.Remove(prop); } else { prop.Count = count; } var update = Builders.Update.Set(p => p.Props, user.Props); var (ok, message) = await _userMongo.UpdateByIdAsync(user.Id, update); if (ok) { _capPublisher.Publish("BackgroundOperation.NotifyUser", new BackgroundOperationNotifyMessage { UserId = userId, Action = count <= 0 ? "backpack_delete" : "backpack_update", PropNumber = propNumber, Count = count }); } return (ok, message); } public async Task<(bool isok, string msg)> DeleteUserProp(string userId, string propNumber) { var user = await _userMongo.FindByIdAsync(userId); if (user == null) return (false, "用户不存在"); var prop = user.Props.FirstOrDefault(p => p.Number == propNumber); if (prop == null) return (false, "道具不存在"); user.Props.Remove(prop); var update = Builders.Update.Set(p => p.Props, user.Props); var (ok, message) = await _userMongo.UpdateByIdAsync(user.Id, update); if (ok) { _capPublisher.Publish("BackgroundOperation.NotifyUser", new BackgroundOperationNotifyMessage { UserId = userId, Action = "backpack_delete", PropNumber = propNumber }); } return (ok, message); } public async Task<(int total, int updated, string msg)> SyncAllUserLevel() { var gameConfig = await _gameConfigService.Get(); if (gameConfig == null) return (0, 0, "游戏配置不存在"); var allUsers = await _userMongo.FilterByAsync(u => !u.IsDelete); int updated = 0; foreach (var user in allUsers) { bool changed = false; var exp = user.Assets.Exp; var levelConfig = gameConfig.UserLevelConfigs .FirstOrDefault(c => exp >= c.ExpMin && exp < c.ExpMax); if (levelConfig != null && levelConfig.Level != user.Assets.Level) { user.Assets.Level = levelConfig.Level; changed = true; } var vipExp = user.Static.TotalRecharge * 10 + user.Assets.VipExpExtra; user.Assets.VipExp = (long)vipExp; var vipLevelConfig = gameConfig.VipLevelConfigs .FirstOrDefault(c => vipExp >= c.ExpMin && vipExp < c.ExpMax); if (vipLevelConfig != null && vipLevelConfig.Level != user.Assets.VipLevel) { user.Assets.VipLevel = vipLevelConfig.Level; user.Assets.VipSignInAdd = vipLevelConfig.SignInAdd; changed = true; } if (changed) { var update = Builders.Update .Set(p => p.Assets.Level, user.Assets.Level) .Set(p => p.Assets.VipLevel, user.Assets.VipLevel) .Set(p => p.Assets.VipExp, user.Assets.VipExp) .Set(p => p.Assets.VipExpExtra, user.Assets.VipExpExtra) .Set(p => p.Assets.VipSignInAdd, user.Assets.VipSignInAdd); await _userMongo.UpdateByIdAsync(user.Id, update); updated++; } } return (allUsers.Count, updated, $"共 {allUsers.Count} 个玩家,更新了 {updated} 个"); } } }