Files
BuYuHouDuan/AdminSrc/AdminService/User/AdminUserService.cs
T

1464 lines
61 KiB
C#
Raw Normal View History

2026-10-12 16:22:04 +08:00
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<UserEntity> _userMongo;
private readonly IMongoBaseRepository<UserEmail> _emailMongo;
private readonly IMongoBaseRepository<UserReward> _rewardMongo;
private readonly ILogger<AdminUserService> _logger;
private readonly IRedisService _cache;
private readonly IGameConfigService _gameConfigService;
private readonly ICapPublisher _capPublisher;
private readonly IDistributedLock _distributedLock;
public AdminUserService(
IMongoBaseRepository<UserEntity> userMongo,
IMongoBaseRepository<UserEmail> emailMongo,
IMongoBaseRepository<UserReward> rewardMongo,
ILogger<AdminUserService> 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<string> 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<string> ids = new List<string>();
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<UserEmail> emails = new List<UserEmail>();
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<UserEntity> list, int count)> GetAdminUserList(AdminUserListParam param)
{
var where = LinqExtensions.True<UserEntity>().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<string>? 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<Func<UserEntity, object>> 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<UserEntity>.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<UserStatistics> 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<EmailStatistics> 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<UserEmail> list, long count)> GetEmailList(EmailListQueryParam param)
{
var keyword = param.UserNumber?.Trim();
List<string> 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<string>();
}
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<Dictionary<string, (string UserName, string UserNumber)>> GetEmailUserNamesAsync(List<string> userIds)
{
var result = new Dictionary<string, (string UserName, string UserNumber)>();
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<string> 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<string> 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<UserEmail>();
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<UserEntity> 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<string> 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<string> 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<PlayerFactor> 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<UserEntity> list, int count)> GetUserFactorList(UserFactorListParam param)
{
var where = LinqExtensions.True<UserEntity>().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<string>? onlineLoginIds = null;
if (param.IsOnline.HasValue)
{
onlineLoginIds = _cache.GetAllLoginUserIds().ToHashSet(StringComparer.Ordinal);
}
List<UserEntity> resultList;
long totalCount;
if (onlineLoginIds != null)
{
var allUsers = await _userMongo.FilterByAsync(where);
IEnumerable<UserEntity> 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<List<UserEntity>> GetUsersByRegisterIps(List<string> registerIps)
{
var filter = Builders<UserEntity>.Filter.In(u => u.RegisterIp, registerIps);
return await _userMongo.FilterByAsync(filter);
}
public async Task<List<BaseProductCount>> GetUserProps(string userId)
{
var user = await _userMongo.FindByIdAsync(userId);
return user?.Props ?? new List<BaseProductCount>();
}
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<UserEntity>.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<UserEntity>.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<UserEntity>.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<UserEntity>.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} 个");
}
}
}