using System.Security.Claims; using System.Text.RegularExpressions; using AdminDomain.Entity; using AutoMapper; using Microsoft.AspNetCore.SignalR; using UserService.Hubs; using Common; using Common.BlockWord; using Common.CommonModel; using Common.Redis; using Common.Utils; using CommonDomain; using CommonDomain.BaseModel; using CommonDomain.Enum; using CommonInfrastructure.Response; using DotNetCore.CAP; using JWTHelper; using Microsoft.Extensions.Logging; using MongoDB.Driver; using MongoRepository; using RedisHelper; using RedisService; using UserDomain.Entity; using UserDomain.Enum; using UserService.GameConfigs; using UserService.Param; using UserService.Response; using UserService.Tasks.Param; using UserService.User.Response; namespace UserService.User { public class UserService : IUserService { private readonly IMongoBaseRepository _userMongo; private readonly IMongoBaseRepository _emailMongo; private readonly IMongoBaseRepository _rewardMongo; private readonly IMongoBaseRepository _orderMongo; private readonly IMongoBaseRepository _beadfulNumberMongo; private readonly IMongoBaseRepository _userBattleOrderMongo; private readonly IGameConfigService _gameConfigService; private readonly IActivityConfigService _activityConfigService; private readonly IMongoBaseRepository _loginLogMongo; private readonly ILogger _logger; private readonly IMapper _mapper; private readonly IRedisService _cache; private readonly JwtTools _jwtTools; private readonly ICapPublisher _capPublisher; private readonly IDistributedLock _distributedLock; private readonly IHubContext _hubContext; public UserService(IMongoBaseRepository userMongo, IMongoBaseRepository emailMongo, IMongoBaseRepository rewardMongo, IMongoBaseRepository orderMongo, IMongoBaseRepository beadfulNumberMongo, IMongoBaseRepository userBattleOrderMongo, IGameConfigService gameConfigService, IActivityConfigService activityConfigService, IMongoBaseRepository loginLogMongo, ILogger logger, IMapper mapper, IRedisService cache, JwtTools jwtTools, ICapPublisher capPublisher, IDistributedLock distributedLock, IHubContext hubContext) { _userMongo = userMongo; _emailMongo = emailMongo; _rewardMongo = rewardMongo; _orderMongo = orderMongo; _beadfulNumberMongo = beadfulNumberMongo; _userBattleOrderMongo = userBattleOrderMongo; _gameConfigService = gameConfigService; _activityConfigService = activityConfigService; _loginLogMongo = loginLogMongo; _logger = logger; _mapper = mapper; _cache = cache; _jwtTools = jwtTools; _capPublisher = capPublisher; _distributedLock = distributedLock; _hubContext = hubContext; } public async Task RegisterWeiXin(RegisterUserWeiXinParam param, string ip) { var user = await _userMongo.FindOneAsync(c => c.BaseInfo.WXOpenId == param.OpenId); if (user != null) { var result = await LoginWx(param.OpenId, ip); if (result != null) return result; return null; } user = new UserEntity(); user.BaseInfo.WXOpenId = param.OpenId; user.BaseInfo.AvatarUrl = param.AvatarUrl; user.Assets = new Assets() { }; user.BaseInfo.Number = await GetUserNumner(); user.BaseInfo.UserName = string.IsNullOrWhiteSpace(param.NickName) ? $"ID_{user.BaseInfo.Number}" : param.NickName.Trim(); var gameConfig = _cache.GetGameConfig(); var activityConfig = await _activityConfigService.Get(); user.Init(gameConfig, activityConfig); user.RegisterIp = ip; user.Static.LastLoginTime = DateTime.Now.ToTimeStamp(); user.Static.LastLoginIp = ip; var res = await _userMongo.InsertOneAsync(user); if (!res.ok) return null; var model = await LoginToken(user); return model; } public async Task<(bool isok, string msg, UserToken user)> Register(RegisterUserParam param, string ip) { if (param == null) throw new BusinessException("注册参数不能为空"); param.Mobile = param.Mobile?.Trim(); param.Account = param.Account?.Trim() ?? ""; param.NickName = param.NickName?.Trim() ?? ""; var mobileMasked = !string.IsNullOrWhiteSpace(param.Mobile) && param.Mobile.Length >= 7 ? $"{param.Mobile[..3]}****{param.Mobile[^4..]}" : param.Mobile ?? string.Empty; if (string.IsNullOrWhiteSpace(param.Mobile) || param.Mobile.Length != 11) throw new BusinessException("手机号格式错误"); if (string.IsNullOrWhiteSpace(param.Account)) throw new BusinessException("账号不能为空"); if (string.IsNullOrWhiteSpace(param.NickName)) throw new BusinessException("昵称不能为空"); { Regex _detailedPhoneRegex = new Regex(@"^1[3-9]\d{9}$", RegexOptions.Compiled); if (_detailedPhoneRegex.IsMatch(param.Mobile) == false) throw new BusinessException("手机号格式错误"); } if (string.IsNullOrWhiteSpace(param.PassWord)) throw new BusinessException("密码不能为空"); param.PassWord = Md5Helper.GetMD5(param.PassWord); var gameConfig = _cache.GetGameConfig(); if (gameConfig == null) throw new BusinessException("游戏配置错误"); var activityConfig = await _activityConfigService.Get(); if (activityConfig == null) throw new BusinessException("活动配置错误"); var lockKey = $"{RedisKey.DistributedLockPrefix}user:register:mobile:{param.Mobile}"; var result = await _distributedLock.TryExecuteAsync(lockKey, async () => { UserEntity user; try { user = await _userMongo.FindOneAsync(c => c.BaseInfo.Mobile == param.Mobile); if (user != null) { throw new BusinessException("手机号已被注册"); } user = await _userMongo.FindOneAsync(c => c.BaseInfo.WXOpenId == param.Account); if (user != null) { throw new BusinessException("账号已被注册"); } } catch (BusinessException) { throw; } catch (Exception ex) { _logger.LogError(ex, "注册查重异常 mobile={Mobile} Account={Account}", mobileMasked, param.Account); throw; } user = new UserEntity(); user.BaseInfo.Mobile = param.Mobile; user.BaseInfo.WXOpenId = param.Account; user.CreateTime = DateTime.Now.ToTimeStamp(); user.BaseInfo.Password = param.PassWord; user.BaseInfo.Number = await GetUserNumner(); user.BaseInfo.UserName = param.NickName; user.Init(gameConfig, activityConfig); user.RegisterIp = ip; user.Static.LastLoginTime = DateTime.Now.ToTimeStamp(); user.Static.LastLoginIp = ip; var res = await _userMongo.InsertOneAsync(user); if (!res.ok) { throw new BusinessException("注册失败"); } var model = await LoginToken(user); return (true, "注册成功", model); }, expiry: 30); if (result == default) throw new BusinessException("系统繁忙,请稍后再试"); return result; } public async Task<(bool isok, string msg)> SendMobileCode(string mobile) { if (!ValidationHelper.IsValidMobile(mobile)) throw new BusinessException("手机号格式错误"); string code = "000000"; { } _cache.SetVerificationCode(mobile, code); return (true, "发送成功"); } public async Task LoginWx(string openId, string ip) { var user = await GetUserByOpenId(openId); if (user == null) { await AddLoginLog(null, null, null, 2, 0, ip, "用户不存在"); return null; } try { user.Static.LastLoginTime = DateTime.Now.ToTimeStamp(); user.Static.LastLoginIp = ip; await _userMongo.ReplaceOneAsync(user); await AddLoginLog(user.Id, user.BaseInfo.Number, user.BaseInfo.UserName, 2, 1, ip, null); } catch { // ignore } await HandleLoginRoomSettlementAsync(user.Id); user = await _userMongo.FindByIdAsync(user.Id); var model = await LoginToken(user); return model; } public async Task<(bool isok, string msg, UserToken user)> Login(string mobile, string code, string ip) { if (string.IsNullOrEmpty(mobile)) throw new BusinessException("请输入手机号"); if (string.IsNullOrEmpty(code)) throw new BusinessException("请输入验证码"); var user = await GetUserByMobile(mobile); if (user == null) { await AddLoginLog(null, null, mobile, 1, 0, ip, "用户不存在"); throw new BusinessException("用户不存在"); } var redisCode = _cache.GetVerificationCode(mobile); var isCodeLogin = !string.IsNullOrEmpty(redisCode) && redisCode == code; var isPasswordLogin = Md5Helper.GetMD5(code) == user.BaseInfo.Password; if (!isCodeLogin && !isPasswordLogin) { await AddLoginLog(user.Id, user.BaseInfo.Number, user.BaseInfo.UserName, 1, 0, ip, "验证码或密码错误"); throw new BusinessException("验证码错误"); } if (isCodeLogin) _cache.RemoveVerificationCode(mobile); user.Static.LastLoginTime = DateTime.Now.ToTimeStamp(); user.Static.LastLoginIp = ip; await _userMongo.ReplaceOneAsync(user); await AddLoginLog(user.Id, user.BaseInfo.Number, user.BaseInfo.UserName, 1, 1, ip, null); await HandleLoginRoomSettlementAsync(user.Id); user = await _userMongo.FindByIdAsync(user.Id); var model = await LoginToken(user); return (true, "登录成功", model); } public async Task<(bool isok, string msg, UserToken user)> LoginByPassword(string mobile, string password, string ip) { if (string.IsNullOrEmpty(mobile)) return (false, "请输入手机号", null); if (string.IsNullOrEmpty(password)) return (false, "请输入密码", null); var user = await GetUserByMobile(mobile); if (user == null) { await AddLoginLog(null, null, mobile, 1, 0, ip, "用户不存在"); return (false, "用户不存在", null); } if (string.IsNullOrEmpty(user.BaseInfo.Password)) { await AddLoginLog(user.Id, user.BaseInfo.Number, user.BaseInfo.UserName, 1, 0, ip, "未设置密码"); return (false, "该账号未设置密码,请使用验证码登录", null); } if (Md5Helper.GetMD5(password) != user.BaseInfo.Password) { await AddLoginLog(user.Id, user.BaseInfo.Number, user.BaseInfo.UserName, 1, 0, ip, "密码错误"); return (false, "密码错误", null); } user.Static.LastLoginTime = DateTime.Now.ToTimeStamp(); user.Static.LastLoginIp = ip; await _userMongo.ReplaceOneAsync(user); await AddLoginLog(user.Id, user.BaseInfo.Number, user.BaseInfo.UserName, 1, 1, ip, null); await HandleLoginRoomSettlementAsync(user.Id); user = await _userMongo.FindByIdAsync(user.Id); var model = await LoginToken(user); return (true, "登录成功", model); } private async Task AddLoginLog(string? userId, string? userNumber, string? userName, int loginType, int status, string ip, string? failReason) { try { var log = new UserLoginLog { UserId = userId, UserNumber = userNumber, UserName = userName, LoginType = loginType, Status = status, Ip = ip, FailReason = failReason, LoginTime = DateTime.Now }; await _loginLogMongo.InsertOneAsync(log); } catch (Exception ex) { _logger.LogError(ex, "记录登录日志失败"); } } public async Task<(bool isok, string msg)> UpdatePassword(string mobile, string password, string code) { if (!ValidationHelper.IsValidMobile(mobile)) throw new BusinessException("手机号格式错误"); if (string.IsNullOrWhiteSpace(password) || password.Length < 6 || password.Length > 20) throw new BusinessException("密码长度应为6-20位"); var redisCode = _cache.GetVerificationCode(mobile); if (redisCode == null) throw new BusinessException("请先发送验证码"); if (redisCode != code) throw new BusinessException("验证码错误"); var user = await GetUserByMobile(mobile); if (user == null) throw new BusinessException("用户不存在"); password = Md5Helper.GetMD5(password); _cache.RemoveVerificationCode(mobile); user.BaseInfo.Password = password; var res = await _userMongo.ReplaceOneAsync(user); if (!res.ok) throw new BusinessException("修改失败"); else return (true, "修改成功"); } public async Task<(bool isok, string msg)> InitUserPassword(string userId) { var user = await _userMongo.FindOneAsync(c => c.Id == userId); if (user == null) return (false, "用户不存在"); user.BaseInfo.Password = Md5Helper.GetMD5("123456"); var res = await _userMongo.ReplaceOneAsync(user); if (!res.ok) return (false, "初始化失败"); return (true, "用户密码已初始化为123456"); } public async Task<(bool isok, string msg)> InitSafeBoxPassword(string userId) { var user = await _userMongo.FindOneAsync(c => c.Id == userId); if (user == null) return (false, "用户不存在"); user.SafeBox.Password = Md5Helper.GetMD5("123456"); var res = await _userMongo.ReplaceOneAsync(user); if (!res.ok) return (false, "初始化失败"); return (true, "保险箱密码已初始化为123456"); } public async Task<(bool isok, string msg)> UpdateUserName(string userName, LoginUser loginUser) { if (string.IsNullOrWhiteSpace(userName) || !ValidationHelper.IsValidUserName(userName)) return (false, "用户名格式错误,支持2-20位中英文、数字、下划线"); var user = await GetUser(loginUser.UserId); if (user == null) throw new BusinessException("用户不存在"); if( BlockWordHelper.IsHaveBlockWord(userName)) return (false, "包含敏感词"); if (user.Props == null || user.Props.Count() == 0) return (false, "请先购买改名卡"); var prop = user.Props.FirstOrDefault(c => c.Type == ProductTypeEnum.改名卡); if (prop == null) return (false, "请先购买改名卡"); if (prop.Count == 0) return (false, "改名卡不足"); if (prop.Count == 1) { user.Props.Remove(prop); } else prop.Count--; user.BaseInfo.UserName = userName; await _userMongo.ReplaceOneAsync(user); return (true, "更新成功"); } public async Task<(bool isok, string msg)> BindWX(string openId, LoginUser loginUser) { if (string.IsNullOrWhiteSpace(openId)) throw new BusinessException("OpenId不能为空"); var user = await GetUser(loginUser.UserId); if (user == null) throw new BusinessException("用户不存在"); if (!string.IsNullOrWhiteSpace(user.BaseInfo.WXOpenId)) throw new BusinessException("微信已绑定"); if (_userMongo.Count(c => c.BaseInfo.WXOpenId == openId) > 0) { throw new BusinessException("此微信号已被绑定,请先解绑"); } user.BaseInfo.WXOpenId = openId; await _userMongo.ReplaceOneAsync(user); return (true, "更新成功"); } public async Task<(bool isok, string msg)> BindMobile(string mobile, string code, LoginUser loginUser) { var redisCode = _cache.GetVerificationCode(mobile); if (redisCode == null) throw new BusinessException("请先发送验证码"); if (redisCode != code) throw new BusinessException("验证码错误"); var user = await GetUser(loginUser.UserId); if (user == null) throw new BusinessException("用户不存在"); if (!string.IsNullOrWhiteSpace(user.BaseInfo.Mobile)) throw new BusinessException("已绑定手机号"); if (_userMongo.Count(c => c.BaseInfo.Mobile == mobile) > 0) { throw new BusinessException("此手机号已被绑定"); } user.BaseInfo.Mobile = mobile; await _userMongo.ReplaceOneAsync(user); return (true, "更新成功"); } public async Task<(bool isok, string msg)> UpdateAvatar(string avatarUrl, LoginUser loginUser) { var user = await GetUser(loginUser.UserId); if (user == null) throw new BusinessException("用户不存在"); user.BaseInfo.AvatarUrl = avatarUrl; await _userMongo.ReplaceOneAsync(user); return (true, "更新成功"); } public async Task<(bool isok, string msg)> UpdateAvatarFrame(string avatarFrame, LoginUser loginUser) { var user = await GetUser(loginUser.UserId); if (user == null) throw new BusinessException("用户不存在"); user.BaseInfo.AvatarFrame = avatarFrame; await _userMongo.ReplaceOneAsync(user); return (true, "更新成功"); } public async Task<(bool isok, string msg)> RealNameAuth(string idCardNumber, string realName, LoginUser loginUser) { if (string.IsNullOrWhiteSpace(idCardNumber)) throw new BusinessException("身份证号不能为空"); if (!ValidationHelper.IsValidIdCard(idCardNumber)) throw new BusinessException("身份证号格式错误"); if (string.IsNullOrWhiteSpace(realName) || realName.Length < 2 || realName.Length > 20) throw new BusinessException("真实姓名格式错误"); var user = await GetUser(loginUser.UserId); if (user == null) throw new BusinessException("用户不存在"); if (user.RealName != null) throw new BusinessException("已完成实名认证"); user.RealName = new RealNameInfo() { RealName = realName, IDCard = idCardNumber }; await _userMongo.ReplaceOneAsync(user); return (true, "实名认证成功"); } public async Task SearchUser(string key) { var model = await _userMongo.FindOneAsync(c => c.Id == key || c.BaseInfo.Number == key || c.BaseInfo.UserName.Contains(key) || c.BaseInfo.Mobile.Contains(key)); if (model == null) return null; if (model.BanInfo != null && model.BanInfo.IsBan) { if (model.BanInfo.BanDays > 0 && 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 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 (model.BanInfo.BanDays > 0 && 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 GetUserByMobile(string mobile) { var model = await _userMongo.FindOneAsync(c => c.BaseInfo.Mobile == mobile); if (model == null) { throw new BusinessException("用户不存在"); } if (model.BanInfo != null && model.BanInfo.IsBan) { if (model.BanInfo.BanDays > 0 && 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 GetUserByNuimber(string number) { var model = await _userMongo.FindOneAsync(c => c.BaseInfo.Number == number); if (model == null) { throw new BusinessException("用户不存在"); } if (model.BanInfo != null && model.BanInfo.IsBan) { if (model.BanInfo.BanDays > 0 && 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 GetUserByOpenId(string openId) { var model = await _userMongo.FindOneAsync(c => c.BaseInfo.WXOpenId == openId); if (model == null) { throw new BusinessException("用户不存在"); } if (model.BanInfo != null && model.BanInfo.IsBan) { if (model.BanInfo.BanDays > 0 && 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)> UpdateUser(UserEntity user) { return await _userMongo.ReplaceOneAsync(user); } public async Task GetRanking(LoginUser loginUser, int type = 1) { RankingResponse result = new RankingResponse(); result.RankingType = type; result.RankingTypeName = type switch { 1 => "金币排名", 2 => "等级排名", 3 => "今日消耗排名", _ => "金币排名" }; System.Linq.Expressions.Expression> sortField; System.Linq.Expressions.Expression> countWhere; Func rankingValue; switch (type) { case 2: sortField = c => c.Assets.Level; countWhere = c => !c.IsDelete && c.Assets.Level > 0; rankingValue = c => c.Assets.Level; break; case 3: sortField = c => c.Static.TotalConsumeGold; countWhere = c => !c.IsDelete && c.Static.TotalConsumeGold > 0; rankingValue = c => c.Static.TotalConsumeGold; break; default: sortField = c => c.Assets.Gold; countWhere = c => !c.IsDelete && c.Assets.Gold > 0; rankingValue = c => c.Assets.Gold; break; } var list = await _userMongo.QueryPageAsync(countWhere, sortField, true, 500, 1); int i = 0; foreach (var item in list.list.Take(50)) { i++; result.UserList.Add(new RankingUserResponse() { AvatarUrl = item.BaseInfo.AvatarUrl, NickName = item.BaseInfo.UserName, Level = item.Assets.Level, RankingValue = rankingValue(item), Mobile = item.BaseInfo.Mobile, Number = item.BaseInfo.Number, UserId = item.Id, UserName = item.BaseInfo.UserName, Rank = i }); } var self = result.UserList.FirstOrDefault(c => c.UserId == loginUser.UserId); if (self != null) { result.User = self; } else { var user = await GetUser(loginUser.UserId); if (user != null) { var rank = type switch { 2 => await _userMongo.CountAsync(c => !c.IsDelete && c.Assets.Level > user.Assets.Level), 3 => await _userMongo.CountAsync(c => !c.IsDelete && c.Static.TotalConsumeGold > user.Static.TotalConsumeGold), _ => await _userMongo.CountAsync(c => !c.IsDelete && c.Assets.Gold > user.Assets.Gold) }; result.User = new RankingUserResponse() { AvatarUrl = user.BaseInfo.AvatarUrl, NickName = user.BaseInfo.UserName, Level = user.Assets.Level, RankingValue = rankingValue(user), Mobile = user.BaseInfo.Mobile, Number = user.BaseInfo.Number, UserId = user.Id, UserName = user.BaseInfo.UserName, Rank = (int)rank + 1 }; } } return result; } public async Task<(List list, int count)> GetUserList(BaseListPageParam param) { var where = LinqExtensions.True().And(e => e.IsDelete == false); if (!string.IsNullOrWhiteSpace(param.KeyWord)) where = where.And(e => e.BaseInfo.UserName.Contains(param.KeyWord) || e.BaseInfo.Mobile.Contains(param.KeyWord) || e.BaseInfo.Number.Contains(param.KeyWord)); var result = await _userMongo.QueryPageAsync(where, c => c.CreateTime, true, param.PageSize ?? 0, param.PageIndex ?? 0); return (result.list, (int)result.count); } public async Task EveryDayTask() { var activityConfig = await _activityConfigService.Get(); var reliefGoldCount = activityConfig?.ReliefConfig?.Count ?? 3; var updateBase = Builders.Update .Set(p => p.Static.OnlineTime, 0) .Set(p => p.Assets.IsVipDayGift, false) .Set(p => p.Static.ReliefGoldCount, reliefGoldCount); await _userMongo.UpdateManyAsync(c => c.Sign != null, updateBase.Set(p => p.Sign.IsSign, false)); await _userMongo.UpdateManyAsync(c => c.Sign == null, updateBase.Set(p => p.Sign, new SignInfo())); var gameConfig = _cache.GetGameConfig(); _cache.SetSystemGoldDay(gameConfig.KillConfig.DaySystemGold); await CleanupExpiredEmails(); await CheckBattleOrderExpired(); } private async Task CheckBattleOrderExpired() { var activityConfig = await _activityConfigService.Get(); var battleOrderConfig = activityConfig?.BattleOrderConfig; if (battleOrderConfig == null || !battleOrderConfig.IsOpen) return; var now = DateTime.Now.ToTimeStamp(); if (battleOrderConfig.EndTime > 0 && battleOrderConfig.EndTime < now) { await _userBattleOrderMongo.DeleteManyAsync(c => true); battleOrderConfig.IsOpen = false; await _activityConfigService.Update(activityConfig); } } public async Task EveryHourTask() { await Task.CompletedTask; } private const int MaxNumberRetryCount = 50; private async Task GetUserNumner() { for (int i = 0; i < MaxNumberRetryCount; i++) { var numner = RandomHelper.RandomNumbers(8); var model = await _beadfulNumberMongo.FindOneAsync(c => c.Number == numner && c.Status != AdminDomain.BeautifulNumber.BeautifulNumberStatus.Normal); if (model != null) continue; var user = _userMongo.FindOne(c => c.BaseInfo.Number == numner); if (user != null) continue; return numner; } throw new BusinessException("无法生成唯一用户编号,请稍后重试"); } public async Task<(bool isok, string msg)> SaveSafeBox(long gold, LoginUser loginUser) { if (gold <= 0) return (false, "存入金额必须大于0"); var lockKey = $"user:safebox:{loginUser.UserId}"; var lockResult = await _distributedLock.TryExecuteAsync(lockKey, async () => { var user = await GetUser(loginUser.UserId); if (user.Assets.Gold < gold) return (false, "金币不足"); if (user.SafeBox == null) user.SafeBox = new SafeBoxInfo(); if (string.IsNullOrWhiteSpace(user.BaseInfo.Mobile)) return (false, "请先绑定手机号"); if (string.IsNullOrWhiteSpace(user.SafeBox.Password)) return (false, "请先设置保险箱密码"); user.Assets.Gold -= gold; var userReward = new UserReward() { UserId = user.Id, ChangeCount = gold, BeforeCount = user.SafeBox.Amount, CurrencyType = 2, Remark = "存入保险箱", Source = RewardSource.保险箱, Type = 1 }; user.SafeBox.Amount += gold; userReward.AfterCount = user.SafeBox.Amount; user.SafeBox.SaveTime = DateTime.Now.ToTimeStamp(); await _rewardMongo.InsertOneAsync(userReward); return await _userMongo.ReplaceOneAsync(user); }, expiry: 10, retry: false); return lockResult; } public async Task<(bool isok, string msg)> GetSafeBox(long gold, string password, LoginUser loginUser) { if (gold <= 0) return (false, "取出金额必须大于0"); var lockKey = $"user:safebox:{loginUser.UserId}"; var lockResult = await _distributedLock.TryExecuteAsync(lockKey, async () => { var user = await GetUser(loginUser.UserId); if (user.SafeBox == null) return (false, "用户保险箱不存在"); if (string.IsNullOrWhiteSpace(password)) return (false, "请输入密码"); if (user.SafeBox.Amount < gold) return (false, "金币不足"); if (user.SafeBox.Password != Md5Helper.GetMD5(password)) return (false, "密码错误"); user.Assets.Gold += gold; var userReward = new UserReward() { UserId = user.Id, ChangeCount = gold, BeforeCount = user.SafeBox.Amount, CurrencyType = 2, Remark = "取出保险箱", Source = RewardSource.保险箱, Type = 2 }; user.SafeBox.Amount -= gold; userReward.AfterCount = user.SafeBox.Amount; await _rewardMongo.InsertOneAsync(userReward); return await _userMongo.ReplaceOneAsync(user); }, expiry: 10, retry: false); return lockResult; } public async Task<(bool isok, string msg)> SetSafeBoxPassword(string password, LoginUser loginUser) { if (string.IsNullOrWhiteSpace(password) || password.Length < 4 || password.Length > 6) return (false, "密码长度应为4-6位"); var user = await GetUser(loginUser.UserId); if (user.SafeBox == null) user.SafeBox = new SafeBoxInfo(); if (string.IsNullOrWhiteSpace(user.BaseInfo.Mobile)) return (false, "请先绑定手机号"); if (!string.IsNullOrWhiteSpace(user.SafeBox.Password)) return (false, "密码已设置,请先重置密码"); user.SafeBox.Password = Md5Helper.GetMD5(password); return await _userMongo.ReplaceOneAsync(user); } public async Task<(bool isok, string msg)> ResetSafeBoxPassword(string password, string code, LoginUser loginUser) { if (string.IsNullOrWhiteSpace(password) || password.Length < 4 || password.Length > 6) return (false, "密码长度应为4-6位"); var user = await GetUser(loginUser.UserId); if (user.SafeBox == null) return (false, "用户保险箱不存在"); if (string.IsNullOrWhiteSpace(password)) return (false, "请输入密码"); if (string.IsNullOrWhiteSpace(user.BaseInfo.Mobile)) return (false, "请先绑定手机号"); if (string.IsNullOrWhiteSpace(code)) return (false, "请输入验证码"); if (code != _cache.GetVerificationCode(user.BaseInfo.Mobile)) return (false, "验证码错误"); user.SafeBox.Password = Md5Helper.GetMD5(password); return await _userMongo.ReplaceOneAsync(user); } public async Task<(bool isok, string msg)> GetReliefGold(LoginUser loginUser) { var user = await GetUser(loginUser.UserId); var activityConfig = await _activityConfigService.Get(); if (activityConfig.ReliefConfig.IsOpen == false) return (false, "当前不能领取救济金"); if (user.Assets.Gold > activityConfig.ReliefConfig.Limit) return (false, "当前金币不满足领取救济金的条件"); if (user.Static.ReliefGoldCount <= 0) return (false, "今日已无领取次数"); user.Static.ReliefGoldCount--; user.Assets.Gold += activityConfig.ReliefConfig.Gold; UserReward userReward = new UserReward() { UserId = user.Id, ChangeCount = activityConfig.ReliefConfig.Gold, CurrencyType = 1, Remark = "领取救济金", Source = RewardSource.救济金, Type = 1, BeforeCount = user.Assets.Gold - activityConfig.ReliefConfig.Gold, AfterCount = user.Assets.Gold }; await _rewardMongo.InsertOneAsync(userReward); var update = Builders.Update .Set(p => p.Assets, user.Assets) .Set(p => p.Static, user.Static); return await _userMongo.UpdateByIdAsync(user.Id, update); } public async Task<(bool isok, string msg)> SendBroadcast(string message, LoginUser loginUser) { if (string.IsNullOrWhiteSpace(message)) return (false, "消息不能为空"); if (message.Length > 20) return (false, "消息不能超过20个字"); if (BlockWordHelper.IsHaveBlockWord(message)) return (false, "消息包含敏感词"); var user = await GetUser(loginUser.UserId); var prop = user.Props?.FirstOrDefault(p => p.Number == "40002"); if (prop == null || prop.Count <= 0) return (false, "大喇叭数量不足"); prop.Count--; if (prop.Count <= 0) user.Props.Remove(prop); var update = Builders.Update.Set(p => p.Props, user.Props); await _userMongo.UpdateByIdAsync(user.Id, update); var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); await _hubContext.Clients.All.SendAsync("Broadcast", new { UserName = user.BaseInfo.UserName, AvatarUrl = user.BaseInfo.AvatarUrl, Level = user.Assets.Level, VipLevel = user.Assets.VipLevel, Message = message, Time = now }); return (true, "发送成功"); } public async Task SendKillAnnouncement(KillAnnounceParam param) { var text = $"恭喜玩家{param.UserName}在{param.GameTypeName}{param.RoomName}捕获 {param.FishName}*{param.Multiple}炮倍 {param.Gold}大奖"; await _hubContext.Clients.All.SendAsync("KillAnnouncement", new { Text = text, UserName = param.UserName, GameTypeName = param.GameTypeName, RoomName = param.RoomName, FishName = param.FishName, Multiple = param.Multiple, Gold = param.Gold, Time = param.Timestamp }); } public async Task<(bool isok, string msg, BaseProductCount model)> SyntheticFragments(string propNumber, LoginUser loginUser) { var user = await GetUser(loginUser.UserId); if (user.Props == null) throw new BusinessException("用户背包不存在"); var propFragment = user.Props.Find(c => c.Number == propNumber); if (propFragment == null) throw new BusinessException("道具不存在"); var gameConfig = _cache.GetGameConfig(); var propFragmentConfig = gameConfig.PropFragments.Find(c => c.Number == propNumber); if (propFragmentConfig == null) throw new BusinessException("道具配置不存在"); if (propFragment.Count < propFragmentConfig.FragmentCount) throw new BusinessException("碎片数量不足合成"); int count = propFragment.Count / propFragmentConfig.FragmentCount; if (propFragment.Count == count * propFragmentConfig.FragmentCount) user.Props.Remove(propFragment); else propFragment.Count -= count * 10; var propConfig = gameConfig.PropConfig.FirstOrDefault(c => c.Number == propFragmentConfig.ProductNumber); if (propConfig == null) throw new BusinessException("合成后道具配置不存在"); var prop = user.Props.Find(c => c.Number == propFragmentConfig.ProductNumber); BaseProduct result = propConfig; if (prop == null) { result = propConfig; prop = _mapper.Map(result); prop.Count = count; if (prop.Count > 0) user.Props.Add(prop); } else { if (!ProductType.MoreProductTypeList.Any(c => c == prop.Type)) throw new BusinessException("已有此道具"); prop.Count += count; } await _userMongo.ReplaceOneAsync(user); prop.Count = count; return (true, "合成成功", prop); } public async Task<(bool isok, string msg)> BuyProps(string propNumber, int count, LoginUser loginUser) { if (string.IsNullOrWhiteSpace(propNumber)) throw new BusinessException("道具编号不能为空"); if (count <= 0 || count > 999) throw new BusinessException("购买数量无效"); var user = await GetUser(loginUser.UserId); if (user.Props == null) user.Props = new List(); var gameConfig = _cache.GetGameConfig(); var propConfig = gameConfig.PropConfig.Find(c => c.Number == propNumber); if (propConfig == null) throw new BusinessException("道具配置不存在"); if (!propConfig.IsSale) throw new BusinessException("该道具暂未开放购买"); if (propConfig.Price <= 0) throw new BusinessException("道具价格异常"); var price = propConfig.Price; if (user.UserType == 2 || user.Assets.IsDealer) price = propConfig.DealerPrice > 0 ? propConfig.DealerPrice : propConfig.Price; else if (user.Assets.VipLevel > 0) price = propConfig.VIPPrice > 0 ? propConfig.VIPPrice : propConfig.Price; var totalPrice = price * count; if (user.Assets.Gold < totalPrice) throw new BusinessException("金币不足"); UserReward userReward = new UserReward() { UserId = user.Id, ChangeCount = totalPrice, CurrencyType = 1, Remark = $"购买道具 x{count}", Source = RewardSource.道具, Type = 2, BeforeCount = user.Assets.Gold }; user.Static.TotalConsumeGold += totalPrice; user.Assets.Gold -= totalPrice; if (!ProductType.MoreProductTypeList.Any(c => c == propConfig.Type)) { if (propConfig.Type == ProductTypeEnum.炮台) { var prop = user.CannonList.Find(c => c.Number == propNumber); if (prop != null) throw new BusinessException("已有此炮台"); var cannon = _mapper.Map(propConfig); if (propConfig.Day > 0) { cannon.IsTrial = true; cannon.TrialEndTime = DateTime.Now.AddDays(propConfig.Day).ToTimeStamp(); } user.CannonList.Add(cannon); } if (propConfig.Type == ProductTypeEnum.翅膀) { var prop = user.WingList.Find(c => c.Number == propNumber); if (prop != null) throw new BusinessException("已有此翅膀"); var cannon = _mapper.Map(propConfig); if (propConfig.Day > 0) { cannon.IsTrial = true; cannon.TrialEndTime = DateTime.Now.AddDays(propConfig.Day).ToTimeStamp(); } user.WingList.Add(cannon); } if (propConfig.Type == ProductTypeEnum.战神) { var prop = user.GodWarList.Find(c => c.Number == propNumber); if (prop != null) throw new BusinessException("已有此战神"); var cannon = _mapper.Map(propConfig); if (propConfig.Day > 0) { cannon.IsTrial = true; cannon.TrialEndTime = DateTime.Now.AddDays(propConfig.Day).ToTimeStamp(); } user.GodWarList.Add(cannon); } } else { var prop = user.Props.Find(c => c.Number == propNumber); if (prop != null) { prop.Count += propConfig.SaleCount * count; } else { var result = propConfig; prop = _mapper.Map(result); prop.Count = propConfig.SaleCount * count; if (prop.Count > 0) user.Props.Add(prop); } } await _rewardMongo.InsertOneAsync(userReward); return await _userMongo.ReplaceOneAsync(user); } public async Task<(bool isok, string msg)> UseVipExp(int count, LoginUser loginUser) { if (count <= 0 || count > 999) return (false, "数量无效"); var user = await GetUser(loginUser.UserId); if (user.Props == null || user.Props.Count == 0) return (false, "背包中没有VIP经验包"); var prop = user.Props.Find(c => c.Type == ProductTypeEnum.VipExp); if (prop == null || prop.Count < count) return (false, "VIP经验包数量不足"); prop.Count -= count; if (prop.Count <= 0) user.Props.Remove(prop); user.Assets.VipExpExtra += 100L * count; var gameConfig = _cache.GetGameConfig(); if (gameConfig != null) { var newVipLevels = user.VipLevelVerify(gameConfig); if (newVipLevels != null && newVipLevels.Count > 0) await AutoDistributeVipGift(user, newVipLevels, gameConfig); } await _userMongo.ReplaceOneAsync(user); return (true, "使用成功"); } public async Task<(bool isok, string msg)> BuyGold(string number, int count, LoginUser loginUser) { if (string.IsNullOrWhiteSpace(number)) throw new BusinessException("金币商品编号不能为空"); if (count <= 0 || count > 999) throw new BusinessException("购买数量无效"); var activityConfig = await _activityConfigService.Get(); var config = activityConfig.GoldShopConfigs.FirstOrDefault(c => c.Number == number); if (config == null) throw new BusinessException("金币配置不存在"); var lockKey = $"user:asset:update:{loginUser.UserId}"; return await _distributedLock.TryExecuteAsync(lockKey, async () => { var user = await GetUser(loginUser.UserId); var diamondPrice = config.Diamond; if (user.UserType == 2 || user.Assets.IsDealer) diamondPrice = config.DealerDiamond > 0 ? config.DealerDiamond : config.Diamond; if (diamondPrice <= 0) throw new BusinessException("金币商品钻石价格配置无效"); var totalDiamond = checked(diamondPrice * count); var totalGold = checked(config.Gold * count); if (totalGold <= 0) throw new BusinessException("金币商品数量配置无效"); if (user.Assets.Diamond < totalDiamond) throw new BusinessException("钻石不足"); var beforeDiamond = user.Assets.Diamond; var beforeGold = user.Assets.Gold; user.Assets.Diamond -= totalDiamond; user.Assets.Gold += totalGold; user.Static.TotalConsumeDiamond += totalDiamond; user.Static.TotalRechargeGold += totalGold; var updateDefinition = Builders.Update .Set(p => p.Assets, user.Assets) .Set(p => p.Static, user.Static); var updateResult = await _userMongo.UpdateByIdAsync(user.Id, updateDefinition); if (!updateResult.ok) return (false, updateResult.message); try { await _rewardMongo.InsertOneAsync(new UserReward { UserId = user.Id, ChangeCount = totalDiamond, CurrencyType = 3, Remark = $"购买金币 x{count}", Source = RewardSource.金币商城, Type = 2, BeforeCount = beforeDiamond, AfterCount = user.Assets.Diamond }); await _rewardMongo.InsertOneAsync(new UserReward { UserId = user.Id, ChangeCount = totalGold, CurrencyType = 1, Remark = $"购买金币 x{count}", Source = RewardSource.金币商城, Type = 1, BeforeCount = beforeGold, AfterCount = user.Assets.Gold }); } catch (Exception ex) { _logger.LogError(ex, "[金币商城] 资产已更新但流水写入失败 userId={UserId} number={Number} count={Count}", user.Id, number, count); } try { await Task.WhenAll( _capPublisher.PublishAsync(TopicKeys.用户资产同步前端, new UserAssetSyncParam { UserId = user.Id, CurrencyType = 3, ChangeType = (int)UserAssetChangeTypeEnum.减少, Amount = totalDiamond, NewGold = user.Assets.Gold, NewDiamond = user.Assets.Diamond, Source = RewardSource.金币商城.ToString(), Remark = $"购买金币 x{count}" }), _capPublisher.PublishAsync(TopicKeys.用户资产同步前端, new UserAssetSyncParam { UserId = user.Id, CurrencyType = 1, ChangeType = (int)UserAssetChangeTypeEnum.增加, Amount = totalGold, NewGold = user.Assets.Gold, NewDiamond = user.Assets.Diamond, Source = RewardSource.金币商城.ToString(), Remark = $"购买金币 x{count}" })); } catch (Exception ex) { _logger.LogError(ex, "[金币商城] 资产实时同步失败 userId={UserId} number={Number} count={Count} newGold={Gold} newDiamond={Diamond}", user.Id, number, count, user.Assets.Gold, user.Assets.Diamond); } return (true, updateResult.message); }, expiry: 10, retry: true, retryDelay: 100); } public async Task<(bool isok, string msg, string url)> BuyDiamond(string number, int count, LoginUser loginUser) { if (string.IsNullOrWhiteSpace(number)) throw new BusinessException("钻石商品编号不能为空"); if (count <= 0 || count > 999) throw new BusinessException("购买数量无效"); var user = await GetUser(loginUser.UserId); var activityConfig = await _activityConfigService.Get(); var config = activityConfig.DiamondShopConfigs.FirstOrDefault(c => c.Number == number); if (config == null) throw new BusinessException("钻石配置不存在"); var price = config.Money; if (user.UserType == 2 || user.Assets.IsDealer) price = config.DealerMoney > 0 ? config.DealerMoney : config.Money; var totalPrice = price * count; var order = new Order(); order.Type = 2; order.ProductId = config.Number; order.ProductName = config.Name + $" x{count}"; order.UserId = loginUser.UserId; order.State = 1; order.Price = totalPrice; await _orderMongo.InsertOneAsync(order); BuyDiamondPayCallBack(order.Id); return (true, "购买成功", ""); } public async Task<(bool isok, string msg)> BuyDiamondPayCallBack(string orderId) { var order = await _orderMongo.FindOneAsync(c => c.Id == orderId); if (order == null) throw new BusinessException("订单不存在"); if (order.State != 1) throw new BusinessException("订单状态不对"); var gameconfig = _cache.GetGameConfig(); var activityConfig = await _activityConfigService.Get(); var config = activityConfig.DiamondShopConfigs.FirstOrDefault(c => c.Number == order.ProductId); if (config == null) throw new BusinessException("钻石配置不存在"); await UpdateAssets(new UserAssetParam() { UserId = order.UserId, ChangeType = UserAssetChangeTypeEnum.增加, Diamond = config.Diamond, Remark = $"购买钻石-{order.ProductName}", Source = RewardSource.充值 }); order.State = 2; order.PayTime = DateTime.Now.ToTimeStamp(); await _orderMongo.ReplaceOneAsync(order); var user = await GetUser(order.UserId); user.Static.TotalRecharge += order.Price; var newVipLevels = user.VipLevelVerify(gameconfig); if (newVipLevels != null && newVipLevels.Count > 0) { await AutoDistributeVipGift(user, newVipLevels, gameconfig); } await _userMongo.ReplaceOneAsync(user); if (activityConfig.BattleOrderConfig.IsOpen) { _capPublisher.PublishAsync(TopicKeys.任务完成, new CompletedTaskParam() { taskEvent = TaskEvent.充值任务, userId = user.Id, count = order.Price }); } return (true, "购买成功"); } public async Task<(bool isok, string msg, SignRewardResult result)> Sign(LoginUser loginUser) { var user = await GetUser(loginUser.UserId); if (user == null) throw new BusinessException("用户不存在"); var activityConfig = await _activityConfigService.Get(); var signConfig = activityConfig?.SignActivityConfig; if (signConfig == null || !signConfig.IsOpen) throw new BusinessException("签到活动未开启"); if (signConfig.Days == null || signConfig.Days.Count == 0) throw new BusinessException("签到配置不存在"); if (user.Sign == null) user.Sign = new SignInfo(); var now = DateTime.Now; var today = now.Date; DateTime? lastSignDate = user.Sign.SignTime > 0 ? user.Sign.SignTime.ToDateTime().Date : (DateTime?)null; if (lastSignDate.HasValue && lastSignDate.Value == today) throw new BusinessException("今日已签到"); var day = CalcSignDay(user.Sign.SignDay, lastSignDate, today, signConfig.CannotInterrupt); var dayConfig = signConfig.Days.FirstOrDefault(c => c.Day == day) ?? signConfig.Days.OrderBy(c => c.Day).First(); if (dayConfig == null || dayConfig.GiftList == null || dayConfig.GiftList.Count == 0) throw new BusinessException($"第{day}天签到奖励未配置"); var gift = GetGift(dayConfig.GiftList); var gold = Convert.ToInt64(gift.Gold * (1 + user.Assets.VipSignInAdd)); var res = await UpdateAssets(new UserAssetParam { UserId = user.Id, ChangeType = UserAssetChangeTypeEnum.增加, Source = RewardSource.签到, Remark = $"第{day}天签到奖励", Gold = gold, Diamond = gift.Diamond, RewardItems = gift.Product }); if (!res.isok) return (false, res.msg, null); var signUpdate = Builders.Update .Set(p => p.Sign.IsSign, true) .Set(p => p.Sign.SignTime, now.ToTimeStamp()) .Set(p => p.Sign.SignDay, day); await _userMongo.UpdateByIdAsync(user.Id, signUpdate); return (true, "签到成功", new SignRewardResult { Day = day, Gold = gold, Diamond = gift.Diamond, Product = gift.Product ?? new List() }); } public async Task GetSignStatus(LoginUser loginUser) { var user = await GetUser(loginUser.UserId); var activityConfig = await _activityConfigService.Get(); var signConfig = activityConfig?.SignActivityConfig; var result = new SignStatusResult { IsOpen = signConfig?.IsOpen ?? false, CannotInterrupt = signConfig?.CannotInterrupt ?? false, Days = signConfig?.Days?.OrderBy(c => c.Day).ToList() ?? new List() }; var today = DateTime.Now.Date; DateTime? lastSignDate = user?.Sign != null && user.Sign.SignTime > 0 ? user.Sign.SignTime.ToDateTime().Date : (DateTime?)null; result.SignDay = user?.Sign?.SignDay ?? 0; result.IsTodaySigned = lastSignDate.HasValue && lastSignDate.Value == today; result.TodayDay = result.IsTodaySigned ? result.SignDay : CalcSignDay(result.SignDay, lastSignDate, today, result.CannotInterrupt); return result; } private const int SignCycleDays = 7; /// 计算本次签到对应的天数(1~7) private static int CalcSignDay(int currentDay, DateTime? lastSignDate, DateTime today, bool cannotInterrupt) { // 上次签到为昨天:连续签到,天数顺延 if (lastSignDate.HasValue && lastSignDate.Value.Date == today.AddDays(-1)) return currentDay > 0 ? currentDay % SignCycleDays + 1 : 1; // 漏签:不可中断则继续顺延(累计循环);可中断则重置回第1天 if (currentDay > 0 && cannotInterrupt) return currentDay % SignCycleDays + 1; return 1; } public async Task<(bool isok, string msg)> ChangeCannon(string number, LoginUser loginUser) { var user = await GetUser(loginUser.UserId); var connon = user.CannonList.Find(c => c.Number == number); if (connon == null) throw new BusinessException("未找到该炮台"); user.CannonUsing = connon; return await _userMongo.ReplaceOneAsync(user); } public async Task<(bool isok, string msg)> ChangeWing(string number, LoginUser loginUser) { var user = await GetUser(loginUser.UserId); var connon = user.WingList.Find(c => c.Number == number); if (connon == null) throw new BusinessException("未找到该翅膀"); user.WingUsing = connon; return await _userMongo.ReplaceOneAsync(user); } public async Task<(bool isok, string msg)> ChangeGodWar(string number, LoginUser loginUser) { var user = await GetUser(loginUser.UserId); var connon = user.GodWarList.Find(c => c.Number == number); if (connon == null) throw new BusinessException("未找到该战神"); user.GodWarUsing = connon; return await _userMongo.ReplaceOneAsync(user); } public async Task<(bool isok, string msg)> UnloadCannon(string number, LoginUser loginUser) { var user = await GetUser(loginUser.UserId); if (user.CannonUsing?.Number != number) throw new BusinessException("当前未使用该炮台"); user.CannonList ??= new List(); const string defaultCannonNumber = "11001"; var defaultCannon = user.CannonList.FirstOrDefault(c => c.Number == defaultCannonNumber); if (defaultCannon == null) { var defaultCannonConfig = _cache.GetGameConfig()?.PropConfig? .FirstOrDefault(c => c.Number == defaultCannonNumber && c.Type == ProductTypeEnum.炮台); defaultCannon = defaultCannonConfig == null ? new BaseUserProp { Number = defaultCannonNumber, Name = "深海之眼", Desc = "深海之眼", ImageUrl = "/uploads/11001.png", Type = ProductTypeEnum.炮台 } : _mapper.Map(defaultCannonConfig); user.CannonList.Add(defaultCannon); } user.CannonUsing = defaultCannon; return await _userMongo.ReplaceOneAsync(user); } public async Task<(bool isok, string msg)> UnloadWing(string number, LoginUser loginUser) { var user = await GetUser(loginUser.UserId); if (user.WingUsing?.Number != number) throw new BusinessException("当前未使用该翅膀"); user.WingUsing = null; return await _userMongo.ReplaceOneAsync(user); } public async Task<(bool isok, string msg)> UnloadGodWar(string number, LoginUser loginUser) { var user = await GetUser(loginUser.UserId); if (user.GodWarUsing?.Number != number) throw new BusinessException("当前未使用该战神"); user.GodWarUsing = null; return await _userMongo.ReplaceOneAsync(user); } public async Task<(bool isok, string msg, GiftResponse model)> GetVipDayGift(LoginUser loginUser) { var user = await GetUser(loginUser.UserId); if (user.Assets.VipLevel == 0) throw new BusinessException("非VIP无法领取"); if (user.Assets.IsVipDayGift == true) throw new BusinessException("今天已经领取过了"); var gameConfig = _cache.GetGameConfig(); var vipConfig = gameConfig.VipLevelConfigs.Find(c => c.Level == user.Assets.VipLevel); if (vipConfig == null) throw new BusinessException("Vip配置不存在"); if (vipConfig.DailyGift == null || vipConfig.DailyGift.Count == 0) throw new BusinessException("Vip每日礼包配置不存在"); var result = GetGift(vipConfig.DailyGift); user.Assets.IsVipDayGift = true; await _userMongo.ReplaceOneAsync(user); var res = await UpdateAssets(new UserAssetParam() { ChangeType = UserAssetChangeTypeEnum.增加, Gold = result.Gold, Diamond = result.Diamond, UserId = user.Id, Remark = "Vip每日礼包", Source = RewardSource.Vip每日礼包, RewardItems = result.Product }); if (res.isok) return (true, "领取成功", result); else return (false, res.msg, null); } public async Task<(bool isok, string msg, GiftResponse model)> GetVipGift(int vipLevel, LoginUser loginUser) { var user = await GetUser(loginUser.UserId); if (user.Assets.VipLevel == 0) throw new BusinessException("非VIP无法领取"); if (user.Assets.VipGift == null || user.Assets.VipGift.Count == 0) throw new BusinessException("没有需要领取的礼包"); if (!user.Assets.VipGift.Any(c => c == vipLevel)) throw new BusinessException("没有需要领取的礼包"); var level = user.Assets.VipGift.FirstOrDefault(c => c == vipLevel); var gameConfig = _cache.GetGameConfig(); var vipConfig = gameConfig.VipLevelConfigs.Find(c => c.Level == vipLevel); if (vipConfig == null) throw new BusinessException("Vip配置不存在"); if (vipConfig.GiftList == null || vipConfig.GiftList.Count == 0) throw new BusinessException("Vip权益礼包配置不存在"); var result = GetGift(vipConfig.GiftList); user.Assets.VipGift.Remove(level); await _userMongo.ReplaceOneAsync(user); var res = await UpdateAssets(new UserAssetParam() { ChangeType = UserAssetChangeTypeEnum.增加, Gold = result.Gold, Diamond = result.Diamond, UserId = user.Id, Remark = "Vip权益礼包", Source = RewardSource.Vip权益礼包, RewardItems = result.Product }); if (res.isok) return (true, "领取成功", result); else return (false, res.msg, null); } public async Task<(List list, int count)> GetUserRewardList(GetUserRewardListParam param) { var where = LinqExtensions.True().And(e => e.IsDelete == false); if (!string.IsNullOrWhiteSpace(param.UserId)) where = where.And(e => e.UserId == param.UserId); if (param.Type != 0) where = where.And(e => e.Type == param.Type); if (param.Source >0) where = where.And(e => (int)e.Source == param.Source); if (param.CurrencyType != 0) where = where.And(e => e.CurrencyType == param.CurrencyType); var result = await _rewardMongo.QueryPageAsync(where, c => c.CreateTime, true, param.PageSize ?? 0, param.PageIndex ?? 0); return (result.list, (int)result.count); } public async Task GameCoinsSettlement(GameCoinsSettlementParam param) { var session = _cache.GetRedisUserCId(param.UserId); var currentRoom = !string.IsNullOrWhiteSpace(session?.RoomId) ? _cache.GetGameRoom(session.RoomId) : null; var currentPlayer = currentRoom?.Players?.FirstOrDefault(p => p.UserId == param.UserId && !p.IsBot); if (session?.RoomId != param.RoomInfo || currentPlayer == null || (param.RoomSessionId > 0 && currentPlayer.BeginTime != param.RoomSessionId)) { return currentPlayer?.Gold ?? param.AfterGold; } var lockKey = $"user:asset:update:{param.UserId}"; var settled = await _distributedLock.TryExecuteAsync(lockKey, async () => { var user = await GetUser(param.UserId); if (user == null) { return param.AfterGold; } long netChange = param.AfterGold - param.BeforeGold; long beforeGold = user.Assets.Gold; long newGold = Math.Max(0, user.Assets.Gold + netChange); user.Assets.Gold = newGold; var type = netChange >= 0 ? 1 : 2; var changeCount = Math.Abs(netChange); var model = new UserReward() { UserId = param.UserId, BeforeCount = beforeGold, Source = RewardSource.游戏金币结算, Remark = "游戏金币结算", ChangeCount = changeCount, Type = type, CurrencyType = 1, RoomName = param.RoomName, RoomInfo = param.RoomInfo, AfterCount = newGold }; await _rewardMongo.InsertOneAsync(model); var updateDefinition = Builders.Update.Set(p => p.Assets, user.Assets); await _userMongo.UpdateByIdAsync(user.Id, updateDefinition); try { await _capPublisher.PublishAsync(TopicKeys.用户资产同步前端, new UserAssetSyncParam { UserId = param.UserId, CurrencyType = 1, ChangeType = type, Amount = changeCount, NewGold = newGold, NewDiamond = user.Assets.Diamond, Source = "游戏金币结算", Remark = param.RoomName }); } catch (Exception ex) { _logger.LogError(ex, "游戏金币结算后发布资产更新失败 userId={UserId}", param.UserId); } return newGold; }, expiry: 10, retry: true, retryDelay: 100); return settled; } private async Task HandleLoginRoomSettlementAsync(string userId) { try { var userRedis = _cache.GetRedisUserCId(userId); var roomId = userRedis?.RoomId; if (string.IsNullOrWhiteSpace(roomId)) roomId = _cache.GetPendingFishRoomId(userId); if (string.IsNullOrWhiteSpace(roomId)) return; var room = _cache.GetGameRoom(roomId); if (room == null) { if (userRedis != null) { userRedis.RoomId = ""; userRedis.CId = ""; _cache.SetRedisUserCId(userRedis); } return; } var player = room.Players?.FirstOrDefault(p => p.UserId == userId && !p.IsBot); if (player == null) { if (userRedis != null) { userRedis.RoomId = ""; userRedis.CId = ""; _cache.SetRedisUserCId(userRedis); } return; } await GameCoinsSettlement(new GameCoinsSettlementParam { UserId = userId, BeforeGold = player.BeginCoin, AfterGold = player.Gold, RoomSessionId = player.BeginTime, RoomInfo = roomId, RoomName = $"{room.GameType}-{room.RoomName}" }); room.Players.Remove(player); _cache.SetGameRoom(room); if (userRedis != null) { userRedis.RoomId = ""; userRedis.CId = ""; _cache.SetRedisUserCId(userRedis); } } catch (Exception ex) { _logger.LogError(ex, "[登录结算] 处理失败 userId={UserId}", userId); } } 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 = true; if (email.RewardGold > 0 || email.RewardDiamond > 0 || (email.RewardItems != null && email.RewardItems.Count > 0)) { 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); return (true, "发送成功"); } public async Task CleanupExpiredEmails() { const int MAX_EMAIL_COUNT = 200; const int EMAIL_EXPIRE_DAYS = 30; var now = DateTime.Now.ToTimeStamp(); var expireTime = DateTime.Now.AddDays(-EMAIL_EXPIRE_DAYS).ToTimeStamp(); var users = await _userMongo.FilterByAsync(c => c.IsDelete == false); foreach (var user in users) { var emails = await _emailMongo.FilterByAsync(c => c.UserId == user.Id); if (emails == null || emails.Count == 0) continue; var sortedEmails = emails.OrderByDescending(c => c.SendTime).ToList(); if (sortedEmails.Count > MAX_EMAIL_COUNT) { var deleteIds = sortedEmails.Skip(MAX_EMAIL_COUNT).Select(c => c.Id).ToList(); foreach (var deleteId in deleteIds) { await _emailMongo.DeleteByIdAsync(deleteId); } } var expiredEmails = emails.Where(c => c.SendTime < expireTime || (c.ExpireTime > 0 && c.ExpireTime < now)).ToList(); foreach (var expiredEmail in expiredEmails) { await _emailMongo.DeleteByIdAsync(expiredEmail.Id); } } } public async Task GetEmail(string id, LoginUser loginUser) { var email = await _emailMongo.FindByIdAsync(id); if (email == null) return null; if (email.UserId != loginUser.UserId) return null; if (!email.IsRead) { email.ReadTime = DateTime.Now.ToTimeStamp(); email.IsRead = true; await _emailMongo.ReplaceOneAsync(email); } EnrichEmailItems(email); return email; } public async Task<(bool isok, string msg, RewardEmailResponse model)> RewardEmail(int type, string id, LoginUser loginUser) { if (type == 1) { var email = await _emailMongo.FindByIdAsync(id); if (email == null) throw new BusinessException("邮件不存在"); if (email.UserId != loginUser.UserId) throw new BusinessException("邮件不存在"); if (email.IsReward) throw new BusinessException("邮件已领取" ); if (email.RewardGold > 0 || email.RewardDiamond > 0 || (email.RewardItems != null && email.RewardItems.Count > 0)) { var updateResult = await UpdateAssets(new UserAssetParam() { Diamond = email.RewardDiamond, Gold = email.RewardGold, UserId = email.UserId, ChangeType = UserAssetChangeTypeEnum.增加, RewardItems = email.RewardItems, Source = RewardSource.邮件, Remark = "领取邮件奖励" }); if (!updateResult.isok) throw new BusinessException(updateResult.msg); email.IsReward = true; email.IsRead = true; email.ReadTime = DateTime.Now.ToTimeStamp(); await _emailMongo.ReplaceOneAsync(email); return (true, "领取成功", new RewardEmailResponse { RewardGold = email.RewardGold, RewardDiamond = email.RewardDiamond, RewardItems = email.RewardItems }); } else { email.IsReward = true; await _emailMongo.ReplaceOneAsync(email); return (true, "无需领取",null); } } else { var emails = await _emailMongo.FilterByAsync(c => c.UserId == loginUser.UserId && !c.IsReward); if (emails == null || emails.Count() == 0) return (true, "无需领取", null); long RewardGold = 0; int RewardDiamond = 0; Dictionary RewardItems = new Dictionary(); foreach (var email in emails) { RewardGold += email.RewardGold; RewardDiamond += email.RewardDiamond; if (email.RewardItems != null) { foreach (var item in email.RewardItems) { if (RewardItems.TryGetValue(item.Number, out var existing)) existing.Count += item.Count; else RewardItems[item.Number] = item; } } if (email.RewardGold > 0 || email.RewardDiamond > 0 || (email.RewardItems != null && email.RewardItems.Count > 0)) { var updateResult = await UpdateAssets(new UserAssetParam() { Diamond = email.RewardDiamond, Gold = email.RewardGold, UserId = email.UserId, ChangeType = UserAssetChangeTypeEnum.增加, RewardItems = email.RewardItems, Source = RewardSource.邮件, Remark = "领取邮件奖励" }); if (!updateResult.isok) throw new BusinessException(updateResult.msg); } email.ReadTime = DateTime.Now.ToTimeStamp(); email.IsRead = true; email.IsReward = true; await _emailMongo.ReplaceOneAsync(email); } return (true, "领取成功",new RewardEmailResponse { RewardGold = RewardGold, RewardDiamond = RewardDiamond, RewardItems = RewardItems.Values.ToList() }); } } public async Task<(bool isok, string msg)> DeleteEmail(int type, string id, LoginUser loginUser) { if (type == 1) return await _emailMongo.DeleteByIdAsync(id); return await _emailMongo.DeleteManyAsync(c => c.UserId == loginUser.UserId && c.IsRead && c.IsReward); } public async Task<(bool isok, string msg)> ReadEmailMore(LoginUser loginUser) { var emails = await _emailMongo.FilterByAsync(c => c.UserId == loginUser.UserId && !c.IsRead); if (emails == null || emails.Count() == 0) return (true, "无未读取的邮件"); foreach (var email in emails) { email.ReadTime = DateTime.Now.ToTimeStamp(); email.IsRead = true; await _emailMongo.ReplaceOneAsync(email); } return (true, "读取成功"); } public async Task GetEmail(string id) { var email = await _emailMongo.FindByIdAsync(id); if (email != null) EnrichEmailItems(email); return email; } public async Task GetNoReadCount(LoginUser loginUser) { return await _emailMongo.CountAsync(c => c.UserId == loginUser.UserId && !c.IsRead); } public async Task<(List list, long count)> GetEmailByPage(GetEmailPageParam param, LoginUser loginUser) { var where = LinqExtensions.True().And(e => e.IsDelete == false); if (loginUser != null && !string.IsNullOrEmpty(loginUser.UserId)) where = where.And(e => e.UserId == loginUser.UserId); if (param.ReadStatus > 0) { where = where.And(e => e.IsRead == (param.ReadStatus == 1)); } var result = await _emailMongo.QueryPageAsync(where, c => c.SendTime, true, param.PageSize ?? 0, param.PageIndex ?? 0); foreach (var email in result.list) EnrichEmailItems(email); return (result.list, result.count); } private void EnrichEmailItems(UserEmail email) { if (email?.RewardItems == null || email.RewardItems.Count == 0) return; try { var gameConfig = _cache.GetGameConfig(); if (gameConfig?.PropConfig == null) return; foreach (var item in email.RewardItems) { if (!string.IsNullOrEmpty(item.ImageUrl)) continue; var prop = gameConfig.PropConfig.FirstOrDefault(p => p.Number == item.Number); if (prop == null) continue; item.Name = prop.Name; item.ImageUrl = prop.ImageUrl; item.Desc = prop.Desc; item.Type = prop.Type; item.Level = prop.Level; } } catch (Exception ex) { _logger.LogError(ex, "补全邮件奖励道具信息失败 emailId={EmailId}", email?.Id); } } public async Task<(bool isok, string msg)> UpdateAssets(UserAssetParam param) { var lockKey = $"user:asset:update:{param.UserId}"; var result = await _distributedLock.TryExecuteAsync(lockKey, async () => { var user = await GetUser(param.UserId); if (user == null) throw new BusinessException("用户不存在"); var gameconfig = _cache.GetGameConfig(); if (param.ChangeType == UserAssetChangeTypeEnum.增加) { if (param.Diamond > 0) { var model = new UserReward() { UserId = param.UserId, BeforeCount = user.Assets.Diamond, Source = param.Source, Remark = param.Remark, ChangeCount = param.Diamond, Type = 1, CurrencyType = 3, RoomName = param.RoomName, RoomInfo = param.RoomInfo, Tax = param.Tax, OtherId = param.OtherId, OtherUserId = param.OtherUserId }; user.Assets.Diamond += param.Diamond; model.AfterCount = user.Assets.Diamond; if(param.IsAddRecord) await _rewardMongo.InsertOneAsync(model); } if (param.Gold > 0) { user.Assets.Gold += param.Gold; if (user.Assets.Gold < 0) user.Assets.Gold = 0; var model = new UserReward() { UserId = param.UserId, BeforeCount = user.Assets.Gold - param.Gold, Source = param.Source, Remark = param.Remark, ChangeCount = param.Gold, Type = 1, CurrencyType = 1, RoomName = param.RoomName, RoomInfo = param.RoomInfo, Tax = param.Tax, OtherId = param.OtherId, OtherUserId = param.OtherUserId }; model.AfterCount = user.Assets.Gold; if (param.IsAddRecord) await _rewardMongo.InsertOneAsync(model); } if (param.Exp > 0) { var res = user.LevelVerify(param.Exp, gameconfig.UserLevelConfigs); if (res.isLevel) { var rewardItems = res.list? .Where(item => item != null && item.Count > 0) .ToList(); var hasReward = res.gold > 0 || res.diamond > 0 || (rewardItems != null && rewardItems.Count > 0); if (hasReward) { var email = new UserEmail { UserId = param.UserId, Title = "升级奖励", Content = $"恭喜您升到{user.Assets.Level}级!升级奖励已发送,请点击领取。", SendTime = DateTime.Now.ToTimeStamp(), RewardGold = res.gold, RewardDiamond = res.diamond, RewardItems = rewardItems, IsReward = false, ExpireTime = DateTime.Now.AddDays(30).ToTimeStamp(), CreateInfo = new OptionUserInfo("system", "系统") }; await _emailMongo.InsertOneAsync(email); } } } if (param.RewardItems != null && param.RewardItems.Count > 0) { foreach (var item in param.RewardItems) { var propConfig = gameconfig.PropConfig.FirstOrDefault(c => c.Number == item.Number); if (propConfig == null) return (false, $"道具配置不存在:{item.Number}"); if (!ProductType.MoreProductTypeList.Any(c => c == propConfig.Type)) { if (propConfig.Type == ProductTypeEnum.炮台) { var prop = user.CannonList.Find(c => c.Number == item.Number); if (prop != null) continue; var cannon = _mapper.Map(propConfig); if (propConfig.Day > 0) { cannon.IsTrial = true; cannon.TrialEndTime = DateTime.Now.AddDays(propConfig.Day).ToTimeStamp(); } user.CannonList.Add(cannon); } if (propConfig.Type == ProductTypeEnum.翅膀) { var prop = user.WingList.Find(c => c.Number == item.Number); if (prop != null) continue; var cannon = _mapper.Map(propConfig); if (propConfig.Day > 0) { cannon.IsTrial = true; cannon.TrialEndTime = DateTime.Now.AddDays(propConfig.Day).ToTimeStamp(); } user.WingList.Add(cannon); } if (propConfig.Type == ProductTypeEnum.战神) { var prop = user.GodWarList.Find(c => c.Number == item.Number); if (prop != null) continue; var cannon = _mapper.Map(propConfig); if (propConfig.Day > 0) { cannon.IsTrial = true; cannon.TrialEndTime = DateTime.Now.AddDays(propConfig.Day).ToTimeStamp(); } user.GodWarList.Add(cannon); } } else { var prop = user.Props.Find(c => c.Number == item.Number); if (prop != null) { prop.Count += item.Count; if (prop.Count <= 0) user.Props.Remove(prop); } else { if (item.Count > 0) { var newProp = _mapper.Map(propConfig); newProp.Count = item.Count; user.Props.Add(newProp); } } } } } } else { if (param.Diamond > 0 && user.Assets.Diamond < param.Diamond) { return (false, "钻石数量不足"); } else if(param.Diamond > 0) { var beforeDiamond = user.Assets.Diamond; user.Assets.Diamond -= param.Diamond; user.Static.TotalConsumeDiamond += param.Diamond; var model = new UserReward() { UserId = param.UserId, BeforeCount = beforeDiamond, AfterCount = user.Assets.Diamond, Source = param.Source, Remark = param.Remark, ChangeCount = param.Diamond, Type = 2, CurrencyType = 3, RoomName = param.RoomName, RoomInfo = param.RoomInfo, Tax = param.Tax, OtherId = param.OtherId, OtherUserId = param.OtherUserId }; if (param.IsAddRecord) await _rewardMongo.InsertOneAsync(model); } if (param.Gold > 0 && user.Assets.Gold < param.Gold) { return (false, "金币不足"); } else if (param.Gold > 0) { user.Assets.Gold -= param.Gold; if (user.Assets.Gold < 0) user.Assets.Gold = 0; user.Static.TotalConsumeGold += param.Gold; //{ var model = new UserReward() { UserId = param.UserId, BeforeCount = user.Assets.Gold + param.Gold, Source = param.Source, Remark = param.Remark, ChangeCount = param.Gold, Type = 2, CurrencyType = 1, RoomName = param.RoomName, RoomInfo = param.RoomInfo, Tax = param.Tax, OtherId = param.OtherId, OtherUserId = param.OtherUserId }; model.AfterCount = user.Assets.Gold; if (param.IsAddRecord) await _rewardMongo.InsertOneAsync(model); //} } if (param.RewardItems != null && param.RewardItems.Count > 0) { foreach (var item in param.RewardItems) { var userProp = user.Props.FirstOrDefault(c => c.Number == item.Number); if (userProp == null) throw new BusinessException("没有该物品"); if (userProp.Count < item.Count) return (false, "物品数量不足"); if (userProp.Count == item.Count) user.Props.Remove(userProp); else userProp.Count -= item.Count; } } } if (param.TotalRechargeGoldDelta != 0) { user.Static.TotalRechargeGold += param.TotalRechargeGoldDelta; if (user.Static.TotalRechargeGold < 0) user.Static.TotalRechargeGold = 0; } var updateDefinition = Builders.Update .Set(p => p.Assets, user.Assets).Set(p => p.Props, user.Props).Set(p => p.Static, user.Static) .Set(p => p.CannonList, user.CannonList).Set(p => p.WingList, user.WingList).Set(p => p.GodWarList, user.GodWarList); await _userMongo.UpdateByIdAsync(user.Id, updateDefinition); if (param.SyncToFrontend && param.Exp > 0) { try { await _capPublisher.PublishAsync("BackgroundOperation.NotifyUser", new BackgroundOperationNotifyMessage { UserId = param.UserId, Action = "level_change", NewLevel = user.Assets.Level, NewExp = user.Assets.Exp, NewGold = user.Assets.Gold, NewDiamond = user.Assets.Diamond, Source = param.Source.ToString(), Remark = param.Remark }); } catch (Exception ex) { _logger.LogError(ex, "经验已入库但实时通知失败 userId={UserId} exp={Exp} newLevel={Level} newExp={NewExp}", param.UserId, param.Exp, user.Assets.Level, user.Assets.Exp); } } if (param.SyncToFrontend) { try { if (param.Gold != 0) { await _capPublisher.PublishAsync(TopicKeys.用户资产同步前端, new UserAssetSyncParam { UserId = param.UserId, CurrencyType = 1, ChangeType = (int)param.ChangeType, Amount = param.Gold, NewGold = user.Assets.Gold, NewDiamond = user.Assets.Diamond, Source = param.Source.ToString(), Remark = param.Remark }); } if (param.Diamond != 0) { await _capPublisher.PublishAsync(TopicKeys.用户资产同步前端, new UserAssetSyncParam { UserId = param.UserId, CurrencyType = 3, ChangeType = (int)param.ChangeType, Amount = param.Diamond, NewGold = user.Assets.Gold, NewDiamond = user.Assets.Diamond, Source = param.Source.ToString(), Remark = param.Remark }); } if (param.RewardItems != null && param.RewardItems.Count > 0) { foreach (var item in param.RewardItems.Where(i => i != null && !string.IsNullOrWhiteSpace(i.Number))) { await _hubContext.Clients.Group(param.UserId).SendAsync("BackpackUpdated", new { Action = param.ChangeType == UserAssetChangeTypeEnum.增加 ? "backpack_add" : "backpack_update", PropNumber = item.Number, Count = param.ChangeType == UserAssetChangeTypeEnum.增加 ? item.Count : -item.Count }); } } } catch (Exception ex) { _logger.LogError(ex, "资产更新成功但前端通知失败 userId={UserId} gold={Gold} diamond={Diamond}", param.UserId, param.Gold, param.Diamond); } } return (true, "更新成功"); }, expiry: 10, retry: false); return result; } public async Task DailyRankingReward() { try { var activityConfig = await _activityConfigService.Get(); var cfg = activityConfig?.RankingRewardConfig; if (cfg == null || !cfg.IsEnabled || cfg.Groups == null || cfg.Groups.Count == 0) { return; } foreach (var group in cfg.Groups) { if (group.Rewards == null || group.Rewards.Count == 0) continue; var ranking = await GetRanking(new LoginUser(), group.RankingType); if (ranking?.UserList == null || ranking.UserList.Count == 0) continue; foreach (var reward in group.Rewards) { var rankUsers = ranking.UserList.Where(u => u.Rank >= reward.RankMin && u.Rank <= reward.RankMax).ToList(); foreach (var user in rankUsers) { var rewardItems = new List(); if (reward.Items != null) { foreach (var item in reward.Items) { if (!string.IsNullOrEmpty(item.OtherId)) { rewardItems.Add(new BaseProductCount { Number = item.OtherId, Count = item.Count > 0 ? item.Count : 1 }); } } } var dateStr = DateTime.Now.ToString("yyyy年M月d日"); var email = new UserEmail { UserId = user.UserId, Title = "排行奖励", Content = $"恭喜您在{dateStr} {group.Name}中排名荣获第{user.Rank}名,奖励已发放!请注意查收~", SendTime = DateTime.Now.ToTimeStamp(), RewardGold = reward.Gold, RewardDiamond = reward.Diamond, RewardItems = rewardItems.Count > 0 ? rewardItems : null, IsReward = reward.Gold <= 0 && reward.Diamond <= 0 && (rewardItems == null || rewardItems.Count == 0), ExpireTime = DateTime.Now.AddDays(30).ToTimeStamp(), CreateInfo = new OptionUserInfo("system", "系统") }; await _emailMongo.InsertOneAsync(email); } } } } catch (Exception ex) { _logger.LogError(ex, "DailyRankingReward 执行异常"); } } private bool Random(double probability, int Max) { int randomValue = System.Random.Shared.Next(0, Max); return randomValue < probability * Max; } public GiftResponse GetGift(List giftConfigs) { GiftResponse response = new GiftResponse(); int totalGold = 0; int totalDiamond = 0; var productDict = new Dictionary(); if (giftConfigs != null) { foreach (var config in giftConfigs) { if (config == null) continue; int actualCount = 0; if (config.IsRandom) { int min = Math.Max(0, config.MinCount); int max = Math.Max(min, config.MaxCount); actualCount = System.Random.Shared.Next(min, max + 1); } else { actualCount = config.Count; } switch (config.Type) { case 1: totalGold += actualCount; break; case 2: totalDiamond += actualCount; break; case 3: if (!string.IsNullOrEmpty(config.OtherId) && actualCount > 0) { if (productDict.ContainsKey(config.OtherId)) productDict[config.OtherId] += actualCount; else productDict[config.OtherId] = actualCount; } break; default: break; } } } var productList = new List(); foreach (var kvp in productDict) { productList.Add(new BaseProductCount { Number = kvp.Key, Count = kvp.Value }); } response = new GiftResponse() { Diamond = totalDiamond, Gold = totalGold, Product = productList }; return response; } private async Task AutoDistributeVipGift(UserEntity user, List newVipLevels, GameConfig gameConfig) { foreach (var lvl in newVipLevels) { var vipConfig = gameConfig.VipLevelConfigs.Find(c => c.Level == lvl); if (vipConfig?.GiftList == null || vipConfig.GiftList.Count == 0) continue; var result = GetGift(vipConfig.GiftList); var rewardItems = result.Product? .Where(item => item != null && item.Count > 0) .ToList(); var hasReward = result.Gold > 0 || result.Diamond > 0 || (rewardItems != null && rewardItems.Count > 0); if (hasReward) { var email = new UserEmail { UserId = user.Id, Title = $"VIP{lvl}升级礼包", Content = $"恭喜您升级到VIP{lvl}!升级礼包已发送,请点击领取。", SendTime = DateTime.Now.ToTimeStamp(), RewardGold = result.Gold, RewardDiamond = result.Diamond, RewardItems = rewardItems, IsReward = false, ExpireTime = DateTime.Now.AddDays(30).ToTimeStamp(), CreateInfo = new OptionUserInfo("system", "系统") }; await _emailMongo.InsertOneAsync(email); } user.Assets.VipGift.Remove(lvl); } } private async Task LoginToken(UserEntity user) { var gameConfig = _cache.GetGameConfig(); var oldVipExp = user.Assets.VipExp; var oldVipLevel = user.Assets.VipLevel; var oldVipSignInAdd = user.Assets.VipSignInAdd; var newVipLevels = user.VipLevelVerify(gameConfig); if (newVipLevels != null && newVipLevels.Count > 0) { await AutoDistributeVipGift(user, newVipLevels, gameConfig); } if (oldVipExp != user.Assets.VipExp || oldVipLevel != user.Assets.VipLevel || oldVipSignInAdd != user.Assets.VipSignInAdd || (newVipLevels != null && newVipLevels.Count > 0)) { await _userMongo.ReplaceOneAsync(user); } var model = _mapper.Map(user); var claims = new Claim[] { new Claim("uid", user.Id), new Claim("uname", user.BaseInfo.UserName??""), new Claim("utype", "2"), new Claim("avatar", user.BaseInfo.AvatarUrl??""), new Claim("number",user.BaseInfo.Number) }; model.Token = _jwtTools.CreateToken(claims); //var userList = _userMongo.FilterBy(c => c.Static.LastLoginIp == user.Static.LastLoginIp); //if (userList.Count() >= gameConfig.KillConfig.IPCount) //{ // var updateDefinition = Builders.Update.Set(p => p.Assets.ViolateRate,gameConfig.KillConfig.ViolateValue); // _userMongo.UpdateMany(c => c.Static.LastLoginIp == user.Static.LastLoginIp, updateDefinition); //} if ((await _activityConfigService.Get())?.BattleOrderConfig.IsOpen == true) { _capPublisher.PublishAsync(TopicKeys.任务完成, new CompletedTaskParam() { taskEvent = TaskEvent.登录任务, userId = user.Id }); } return model; } public async Task RecordFishingConsume(long consumeGold, LoginUser loginUser) { var activityConfig = await _activityConfigService.Get(); if (activityConfig?.FishingReturnConfig == null || !activityConfig.FishingReturnConfig.IsEnabled) { return; } var today = DateTime.Now.ToString("yyyyMMdd"); var key = $"fishing:consume:{loginUser.UserId}:{today}"; _cache.IncrBy(key, consumeGold); } private async Task FlushFishingConsumeToDb(string userId) { try { var today = DateTime.Now.ToString("yyyyMMdd"); var yesterday = DateTime.Now.AddDays(-1).ToString("yyyyMMdd"); var redisTodayKey = $"fishing:consume:{userId}:{today}"; var redisYesterdayKey = $"fishing:consume:{userId}:{yesterday}"; var redisTodayVal = _cache.GetString(redisTodayKey); var redisYesterdayVal = _cache.GetString(redisYesterdayKey); long.TryParse(redisTodayVal, out var redisToday); long.TryParse(redisYesterdayVal, out var redisYesterday); if (redisToday <= 0 && redisYesterday <= 0) return; var user = await _userMongo.FindByIdAsync(userId); if (user == null) return; if (user.FishingReturn.LastFishingDate != today) { user.FishingReturn.YesterdayConsume = user.FishingReturn.TodayConsume + redisYesterday; user.FishingReturn.TodayConsume = 0; user.FishingReturn.IsClaimed = false; user.FishingReturn.LastFishingDate = today; } user.FishingReturn.TodayConsume += redisToday; var update = Builders.Update .Set(p => p.FishingReturn, user.FishingReturn); await _userMongo.UpdateByIdAsync(user.Id, update); if (redisToday > 0) _cache.IncrBy(redisTodayKey, -redisToday); if (redisYesterday > 0) _cache.IncrBy(redisYesterdayKey, -redisYesterday); } catch (Exception ex) { _logger.LogError(ex, "刷新捕鱼返利消费数据失败 userId={UserId}", userId); } } public async Task GetFishingReturnInfo(LoginUser loginUser) { var activityConfig = await _activityConfigService.Get(); var result = new FishingReturnInfo(); if (activityConfig?.FishingReturnConfig == null || !activityConfig.FishingReturnConfig.IsEnabled) { return result; } var user = await _userMongo.FindByIdAsync(loginUser.UserId); if (user == null) return result; var today = DateTime.Now.ToString("yyyyMMdd"); var yesterday = DateTime.Now.AddDays(-1).ToString("yyyyMMdd"); var redisTodayKey = $"fishing:consume:{loginUser.UserId}:{today}"; var redisYesterdayKey = $"fishing:consume:{loginUser.UserId}:{yesterday}"; long.TryParse(_cache.GetString(redisTodayKey), out var redisToday); long.TryParse(_cache.GetString(redisYesterdayKey), out var redisYesterday); if (user.FishingReturn.LastFishingDate != today) { var totalYesterday = user.FishingReturn.TodayConsume + redisYesterday; user.FishingReturn.YesterdayConsume = totalYesterday; user.FishingReturn.TodayConsume = 0; user.FishingReturn.IsClaimed = false; user.FishingReturn.LastFishingDate = today; var update = Builders.Update .Set(p => p.FishingReturn, user.FishingReturn); await _userMongo.UpdateByIdAsync(user.Id, update); } return new FishingReturnInfo { TodayConsume = user.FishingReturn.TodayConsume + redisToday, YesterdayConsume = user.FishingReturn.YesterdayConsume, IsClaimed = user.FishingReturn.IsClaimed, LastFishingDate = user.FishingReturn.LastFishingDate, CanClaimGold = (long)(user.FishingReturn.YesterdayConsume * activityConfig.FishingReturnConfig.ReturnRate) }; } public async Task<(bool isok, string msg, long gold)> ClaimFishingReturn(LoginUser loginUser) { var activityConfig = await _activityConfigService.Get(); if (activityConfig?.FishingReturnConfig == null || !activityConfig.FishingReturnConfig.IsEnabled) { return (false, "活动未开启", 0); } await FlushFishingConsumeToDb(loginUser.UserId); var user = await _userMongo.FindByIdAsync(loginUser.UserId); if (user == null) { return (false, "用户不存在", 0); } var today = DateTime.Now.ToString("yyyyMMdd"); if (user.FishingReturn.LastFishingDate != today) { user.FishingReturn.YesterdayConsume = user.FishingReturn.TodayConsume; user.FishingReturn.TodayConsume = 0; user.FishingReturn.IsClaimed = false; user.FishingReturn.LastFishingDate = today; } if (user.FishingReturn.IsClaimed) { return (false, "今日已领取过返利", 0); } if (user.FishingReturn.YesterdayConsume <= 0) { return (false, "昨日无捕鱼消耗,无法领取返利", 0); } long returnGold = (long)(user.FishingReturn.YesterdayConsume * activityConfig.FishingReturnConfig.ReturnRate); if (activityConfig.FishingReturnConfig.DailyMaxReturn > 0 && returnGold > activityConfig.FishingReturnConfig.DailyMaxReturn) { returnGold = activityConfig.FishingReturnConfig.DailyMaxReturn; } if (returnGold <= 0) { return (false, "返利金额为0", 0); } user.Assets.Gold += returnGold; user.FishingReturn.IsClaimed = true; await _userMongo.ReplaceOneAsync(user); return (true, $"领取成功,获得 {returnGold} 金币", returnGold); } } }