using YD_Event.Application.AppUser.Dtos; using YD_Event.Application.Auth; using YD_Event.Core.Entities; using AppUserEntity = YD_Event.Core.Entities.AppUser; using YD_Event.Core.Enum; using Mapster; using EasyCaching.Core; namespace YD_Event.Application.AppUser; /// /// 普通用户服务 /// public class AppUserService : BaseService, ITransient { private readonly ISqlSugarRepository _appUserRep; private readonly ISqlSugarClient _db; private readonly AuthManager _authManager; private readonly IEasyCachingProvider _cachingProvider; public AppUserService(ISqlSugarRepository appUserRep, ISqlSugarClient db, AuthManager authManager, IEasyCachingProvider cachingProvider) : base(appUserRep) { _appUserRep = appUserRep; _db = db; _authManager = authManager; _cachingProvider = cachingProvider; } /// /// 分页查询普通用户 /// /// /// [DisplayName("分页查询普通用户")] [HttpGet("page")] public async Task> Page([FromQuery] AppUserPageInput input) { var query = _appUserRep.AsQueryable() .Where(u => u.PersonType == PersonType.Athlete) // 仅显示真正在小程序端自主注册/登录过的用户:有密码、或有微信昵称/头像 // 单位后台添加后被移除的运动员(无密码、无微信信息)不应出现在此列表 .Where(u => u.Password != null || u.NickName != null || u.Avatar != null) .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) .WhereIF(input.Gender.HasValue, u => u.Gender == input.Gender) .WhereIF(!string.IsNullOrWhiteSpace(input.Mobile), u => u.Mobile.Contains(input.Mobile)) .WhereIF(input.AuditStatus.HasValue, u => u.AuditStatus == input.AuditStatus) .WhereIF(input.StartTime.HasValue, u => u.CreatedTime >= input.StartTime) .WhereIF(input.EndTime.HasValue, u => u.CreatedTime <= input.EndTime) .OrderByDescending(u => u.CreatedTime); var result = await query.Select().ToPagedListAsync(input); // 根据认证类型填充统一的 CertImageUrl 和 CertNumber 字段 foreach (var item in result.Rows) { FillCertFields(item); } return result; } /// /// 根据认证类型填充统一的认证证件字段(CertImageUrl, CertNumber) /// private static void FillCertFields(AppUserOutput output) { output.CertImageUrl = output.AuthType switch { 1 => output.IdCardImageUrl, 2 => output.PassportPhotoUrl, 3 => output.HouseholdPhotoUrl, _ => output.IdCardImageUrl ?? output.HouseholdPhotoUrl ?? output.PassportPhotoUrl }; output.CertNumber = output.AuthType switch { 1 or 3 => output.IdCardNumber, 2 => output.PassportNumber, _ => output.IdCardNumber ?? output.PassportNumber }; } /// /// 获取普通用户详情 /// /// /// [DisplayName("获取普通用户详情")] [HttpGet("{id}")] public async Task Detail([FromRoute] long id) { var output = await _appUserRep.AsQueryable() .Where(u => u.Id == id) .Select() .FirstAsync(); // 根据认证类型填充统一的 CertImageUrl 和 CertNumber 字段 FillCertFields(output); return output; } /// /// 审核普通用户 /// /// /// [DisplayName("审核普通用户")] [HttpPost("audit")] [UnitOfWork] public async Task Audit([FromBody] AuditInput input) { if (input.Ids == null || !input.Ids.Any()) return; if (input.Status != AuditStatus.Pass && input.Status != AuditStatus.Reject) throw Oops.Bah("审核状态不正确"); // 检查是否存在未提交认证申请的用户 var pendingUsers = await _appUserRep.AsQueryable() .Where(u => input.Ids.Contains(u.Id) && u.AuditStatus == AuditStatus.PendingSubmission) .Select(u => u.Name) .ToListAsync(); if (pendingUsers.Any()) { throw Oops.Bah("所选人员中存在未提交认证的申请,请核对后再操作"); } var now = DateTime.Now; var auditUserId = _authManager.UserId; // 审核人与更新时间分别留痕,后续普通编辑不会覆盖审核记录。 await _appUserRep.UpdateAsync(u => new AppUserEntity { AuditStatus = input.Status, AuditRemark = input.Remark, AuditUserId = auditUserId, AuditTime = now, UpdatedTime = now, UpdatedUserId = auditUserId }, u => input.Ids.Contains(u.Id)); } #region 统一成员管理 (领队/教练/运动员) /// /// 统一分页查询成员 /// /// /// [HttpGet("member/page")] [DisplayName("统一分页查询成员")] public async Task> MemberPage([FromQuery] MemberQueryInput input) { var (userId, merchantCertificationId) = await GetCurrentUserAndMerchantCertificationId(); var query = _appUserRep.AsQueryable() .Where(x => x.PersonType == input.PersonType && !x.DeleteMark) .WhereIF(!string.IsNullOrWhiteSpace(input.Name), x => x.Name.Contains(input.Name)) .WhereIF(input.Gender.HasValue, x => x.Gender == input.Gender) .WhereIF(!string.IsNullOrWhiteSpace(input.Mobile), x => x.Mobile.Contains(input.Mobile)) .WhereIF(!string.IsNullOrWhiteSpace(input.IdCardNumber), u => u.IdCardNumber.Contains(input.IdCardNumber)) .WhereIF(input.PersonType == PersonType.Coach && !string.IsNullOrWhiteSpace(input.SkillLevel), x => x.SkillLevel == input.SkillLevel) .WhereIF(input.PersonType == PersonType.Coach && input.CertificationStatus.HasValue, x => x.CertificationStatus == input.CertificationStatus) .WhereIF(input.StartTime.HasValue, u => u.CreatedTime >= input.StartTime) .WhereIF(input.EndTime.HasValue, u => u.CreatedTime <= input.EndTime) .OrderByDescending(x => x.CreatedTime); // 权限过滤 (管理员可看全部,这里简化的商户隔离逻辑) if (merchantCertificationId.HasValue) { query = query.Where(x => x.MerchantCertificationId == merchantCertificationId.Value); } else { // 这里根据实际业务,如果普通用户只能看自己创建的,需要这个过滤 query = query.Where(x => x.CreatedUserId == userId && x.MerchantCertificationId == null); } var list = await query.ToPagedListAsync(input); var result = list.Adapt>(); // 设置Age字段:优先使用数据库中的Age,如果没有则根据BirthDate计算 foreach (var item in result.Rows) { if (item.Age.HasValue) { // 数据库中已有Age,直接使用 continue; } // 数据库中没有Age,根据BirthDate动态计算 if (item.BirthDate.HasValue) { item.Age = MemberOutput.CalculateAgeFromBirthDate(item.BirthDate); } } return result; } /// /// 统一新增/编辑成员 /// /// /// [HttpPost("member/save")] [DisplayName("统一新增/编辑成员")] [UnitOfWork] public async Task SaveUnifiedMember(CreateOrUpdateMemberInput input) { var (userId, merchantCertificationId) = await GetCurrentUserAndMerchantCertificationId(); // 检查是否有跨单位操作权限 if (!merchantCertificationId.HasValue) { throw Oops.Bah("当前用户未关联单位,无法进行操作"); } // 根据认证类型决定查重策略: // - 运动员 + 有认证类型 + 有证件号 → 用证件号查重 // - 其他情况 → 用手机号查重(原逻辑) var mobileIsEmpty = string.IsNullOrWhiteSpace(input.Mobile); bool useCertCheck = input.PersonType == PersonType.Athlete && input.AuthType > 0 && ( ((input.AuthType == 1 || input.AuthType == 3) && !string.IsNullOrWhiteSpace(input.IdCardNumber)) || (input.AuthType == 2 && !string.IsNullOrWhiteSpace(input.PassportNumber)) ); // 查重基础查询(排除自身ID) ISugarQueryable BuildBaseQuery() { var q = _appUserRep.AsQueryable() .Where(x => x.PersonType == input.PersonType && !x.DeleteMark); if (input.Id.HasValue) { q = q.Where(x => x.Id != input.Id.Value); } return q; } // ========== 查重流程(证件号或手机号,流程一致) ========== AppUserEntity? existingInOtherUnit = null; AppUserEntity? existingWithoutUnit = null; if (useCertCheck) { var certQuery = BuildBaseQuery(); // 按认证类型明确匹配不同证件字段 if (input.AuthType == 1) { // 身份证认证 → 用 IdCardNumber 查重 certQuery = certQuery.Where(x => x.IdCardNumber == input.IdCardNumber); } else if (input.AuthType == 2) { // 护照认证 → 用 PassportNumber 查重 certQuery = certQuery.Where(x => x.PassportNumber == input.PassportNumber); } else if (input.AuthType == 3) { // 户口本认证 → 用 IdCardNumber 查重 certQuery = certQuery.Where(x => x.IdCardNumber == input.IdCardNumber); } var certMatches = await certQuery.ToListAsync(); // 本单位同证件号 → 提示重复 var duplicateInSameUnit = certMatches.FirstOrDefault(x => x.MerchantCertificationId == merchantCertificationId); if (duplicateInSameUnit != null) { throw Oops.Bah($"该{GetCertDisplay(input.AuthType)}在本单位已存在相同类型的成员,请核对信息"); } // 其他单位有记录 → 单位更换 existingInOtherUnit = certMatches.FirstOrDefault(x => x.MerchantCertificationId != null && x.MerchantCertificationId != merchantCertificationId); // 未加入单位的记录 → 复用 if (existingInOtherUnit == null) { existingWithoutUnit = certMatches.FirstOrDefault(x => x.MerchantCertificationId == null); } } else { // 原逻辑:按手机号查重 if (!mobileIsEmpty) { existingInOtherUnit = await _appUserRep.AsQueryable() .Where(x => x.Mobile == input.Mobile && x.PersonType == input.PersonType && x.MerchantCertificationId != merchantCertificationId && x.MerchantCertificationId != null && !x.DeleteMark) .FirstAsync(); } } AppUserEntity? entity = null; bool isUnitTransfer = false; bool isNewRecord = false; long? originalMerchantId = null; if (existingInOtherUnit != null) { // 单位更换流程 isUnitTransfer = true; entity = existingInOtherUnit; originalMerchantId = entity.MerchantCertificationId; } else if (existingWithoutUnit != null) { // 找到已存在但未加入任何单位的记录,复用该记录(证件号查重时进入) entity = existingWithoutUnit; entity.UpdatedUserId = _authManager.UserId; entity.UpdatedTime = DateTime.Now; } else { // 按原手机号继续查询(仅当未使用证件号查重时) if (!useCertCheck && !mobileIsEmpty) { var mobileMatch = await _appUserRep.AsQueryable() .Where(x => x.Mobile == input.Mobile && x.PersonType == input.PersonType && x.MerchantCertificationId == null && !x.DeleteMark) .FirstAsync(); if (mobileMatch != null) { entity = mobileMatch; entity.UpdatedUserId = _authManager.UserId; entity.UpdatedTime = DateTime.Now; } else { isNewRecord = true; } } else { // 没有找到任何记录,标记为新增 isNewRecord = true; } } if (input.Id.HasValue) { // 编辑模式 entity = await _appUserRep.GetByIdAsync(input.Id.Value); if (entity == null || entity.DeleteMark) throw Oops.Bah("成员不存在"); // 手机号为空时,不进行手机号重复检测 if (!mobileIsEmpty) { // 检查手机号是否被本单位内同类型的其他记录占用 var duplicateInSameUnit = await _appUserRep.AsQueryable() .Where(x => x.Mobile == input.Mobile && x.PersonType == input.PersonType && x.MerchantCertificationId == merchantCertificationId && !x.DeleteMark && x.Id != input.Id.Value) .FirstAsync(); if (duplicateInSameUnit != null) { throw Oops.Bah($"该手机号在本单位已存在相同类型的成员"); } } entity.UpdatedUserId = _authManager.UserId; entity.UpdatedTime = DateTime.Now; } else if (isNewRecord) { // 新增模式:手机号不为空时,检查本单位内是否有相同手机号且相同类型的记录 if (!mobileIsEmpty) { var existingInSameUnit = await _appUserRep.AsQueryable() .Where(x => x.Mobile == input.Mobile && x.PersonType == input.PersonType && x.MerchantCertificationId == merchantCertificationId && !x.DeleteMark) .FirstAsync(); if (existingInSameUnit != null) { // 本单位内已有相同手机号且相同类型的记录,不允许重复添加 throw Oops.Bah($"该手机号在本单位已存在相同类型的成员"); } } // 新增记录 entity = new AppUserEntity { Id = YitIdHelper.NextId(), PersonType = input.PersonType, MerchantCertificationId = merchantCertificationId, CreatedUserId = userId, CreatedTime = DateTime.Now, Status = AvailabilityStatus.Enable, AuditStatus = AuditStatus.PendingSubmission }; // 单位添加运动员时,验证码改为非必填;仅在手机号+验证码均存在时才校验 if (input.PersonType == PersonType.Athlete && !mobileIsEmpty && !string.IsNullOrWhiteSpace(input.VerificationCode)) { var cacheKey = $"sms:code:{input.Mobile}"; var cachedCodeResult = _cachingProvider.Get(cacheKey); string? cachedCode = cachedCodeResult.Value; if (!cachedCodeResult.HasValue || cachedCode != input.VerificationCode) { throw Oops.Bah("验证码错误或已过期"); } } if (isUnitTransfer) { await _appUserRep.SoftDeleteByIdAsync(existingInOtherUnit.Id); } } // 更新用户信息字段 entity.Name = input.Name; entity.Gender = input.Gender; entity.IdCardNumber = input.IdCardNumber; entity.Mobile = input.Mobile; entity.Remark = input.Remark; entity.BirthDate = input.BirthDate; entity.Age = input.Age; // 单位更换后原审核结论不再适用于新归属,重置为待提交;普通编辑保留审核历史。 if (isUnitTransfer) { entity.AuditStatus = AuditStatus.PendingSubmission; entity.AuditRemark = null; entity.AuditUserId = null; entity.AuditTime = null; } // 根据认证类型把图片URL和证件号写入对应字段,并清空其他类型图片 if (input.AuthType == 1) { entity.AuthType = 1; entity.IdCardImageUrl = input.IdCardImageUrl; entity.IdCardBackImageUrl = input.IdCardBackImageUrl; entity.PassportPhotoUrl = null; entity.PassportNumber = null; entity.HouseholdPhotoUrl = null; } else if (input.AuthType == 2) { entity.AuthType = 2; entity.PassportPhotoUrl = input.PassportPhotoUrl ?? input.IdCardImageUrl; entity.PassportNumber = input.PassportNumber; entity.IdCardImageUrl = null; entity.IdCardBackImageUrl = null; entity.IdCardNumber = null; entity.HouseholdPhotoUrl = null; } else if (input.AuthType == 3) { entity.AuthType = 3; entity.HouseholdPhotoUrl = input.HouseholdPhotoUrl ?? input.IdCardImageUrl; entity.IdCardImageUrl = null; entity.IdCardBackImageUrl = null; entity.PassportPhotoUrl = null; entity.PassportNumber = null; } else { // AuthType == 0 或其他值:未认证,不修改认证相关字段(保持原样) // 但清空可能的残留图片URL,避免脏数据 entity.AuthType = 0; entity.IdCardImageUrl = null; entity.IdCardBackImageUrl = null; entity.PassportPhotoUrl = null; entity.PassportNumber = null; entity.HouseholdPhotoUrl = null; } // 更新认证单位信息 var oldMerchantId = entity.MerchantCertificationId; if (entity.MerchantCertificationId != merchantCertificationId) { entity.MerchantCertificationId = merchantCertificationId; entity.UpdatedUserId = _authManager.UserId; entity.UpdatedTime = DateTime.Now; } if (input.PersonType == PersonType.Coach) { entity.SkillLevel = input.SkillLevel; entity.CertificationStatus = input.CertificationStatus; } // 保存用户信息 await _appUserRep.InsertOrUpdateAsync(entity); // 如果是单位更换,处理团队关系 if (isUnitTransfer && oldMerchantId.HasValue) { await HandleTeamTransfer(entity.Id, oldMerchantId.Value, merchantCertificationId.Value); } } /// /// 处理单位更换时的团队成员关系 /// /// 用户ID /// 原单位ID /// 新单位ID /// private async Task HandleTeamTransfer(long userId, long oldMerchantId, long newMerchantId) { // 从原单位的所有团队中移除该成员 var oldTeamIds = await _db.Queryable() .Where(t => t.MerchantCertificationId == oldMerchantId && !t.DeleteMark) .Select(t => t.Id) .ToListAsync(); if (oldTeamIds.Any()) { await _db.Updateable() .SetColumns(tm => new TeamMember { DeleteMark = true, UpdatedTime = DateTime.Now, UpdatedUserId = _authManager.UserId }, appendColumnsByDataFilter: true) .Where(tm => tm.UserId == userId && oldTeamIds.Contains(tm.TeamId) && !tm.DeleteMark) .ExecuteCommandAsync(); } } /// /// 获取人员类型显示名称 /// /// /// private static string GetPersonTypeDisplay(PersonType personType) { return personType switch { PersonType.Athlete => "运动员", PersonType.Coach => "教练", PersonType.Leader => "领队", _ => "成员" }; } /// /// 获取证件类型显示名称 /// /// /// private static string GetCertDisplay(int authType) { return authType switch { 1 => "身份证号", 2 => "护照号码", 3 => "身份证号", _ => "证件号" }; } #endregion #region 领队/教练管理 (向下兼容) private async Task<(long userId, long? merchantCertificationId)> GetCurrentUserAndMerchantCertificationId() { var userId = _authManager.UserId; // 直接查询单位认证表,检查是否有审核通过的认证记录 var certification = await _db.Queryable() .FirstAsync(c => c.UserId == userId && !c.DeleteMark); // 有审核通过的认证就返回认证ID,否则返回null return (userId, certification?.Id); } private async Task CheckPermission(AppUserEntity entity) { var (userId, merchantCertificationId) = await GetCurrentUserAndMerchantCertificationId(); if (merchantCertificationId.HasValue) { if (entity.MerchantCertificationId != merchantCertificationId.Value) throw Oops.Bah("无权操作该成员"); } else { if (entity.CreatedUserId != userId || entity.MerchantCertificationId != null) throw Oops.Bah("无权操作该成员"); } } /// /// 删除成员(解除与单位的关联,而非删除用户) /// /// /// [DisplayName("删除成员")] [HttpPost("member/delete")] [UnitOfWork] public async Task DeleteMember([FromBody] long id) { var entity = await _appUserRep.GetByIdAsync(id); if (entity == null || entity.DeleteMark) throw Oops.Bah("成员不存在"); await CheckPermission(entity); // 1. 先保存原来的 MerchantCertificationId 用于后续处理 var originalMerchantId = entity.MerchantCertificationId; // 2. 解除用户与单位的关联 entity.MerchantCertificationId = null; entity.UpdatedUserId = _authManager.UserId; entity.UpdatedTime = DateTime.Now; await _appUserRep.UpdateAsync(entity); // 3. 如果原来有关联单位,需要从该单位的所有团队中移除该成员 if (originalMerchantId.HasValue) { // 查找该单位的所有团队 var teamIds = await _db.Queryable() .Where(t => t.MerchantCertificationId == originalMerchantId.Value && !t.DeleteMark) .Select(t => t.Id) .ToListAsync(); if (teamIds.Any()) { // 从所有相关团队中移除该成员 await _db.Updateable() .SetColumns(tm => new TeamMember { DeleteMark = true, UpdatedTime = DateTime.Now, UpdatedUserId = _authManager.UserId }, appendColumnsByDataFilter: true) .Where(tm => tm.UserId == id && teamIds.Contains(tm.TeamId) && !tm.DeleteMark) .ExecuteCommandAsync(); } } } /// /// 批量删除成员(解除与单位的关联,而非删除用户) /// /// /// [HttpPost("member/batchDelete")] [DisplayName("批量删除成员")] [UnitOfWork] public async Task BatchDeleteMember([FromBody] List ids) { if (ids == null || !ids.Any()) throw Oops.Bah("请选择要删除的记录"); // 获取当前用户的单位ID var (_, merchantCertificationId) = await GetCurrentUserAndMerchantCertificationId(); // 1. 解除所有选中用户与当前单位的关联 await _appUserRep.UpdateAsync(x => new AppUserEntity { MerchantCertificationId = null, UpdatedUserId = _authManager.UserId, UpdatedTime = DateTime.Now }, x => ids.Contains(x.Id)); // 2. 如果有单位ID,从该单位的所有团队中移除这些成员 if (merchantCertificationId.HasValue) { // 查找该单位的所有团队 var teamIds = await _db.Queryable() .Where(t => t.MerchantCertificationId == merchantCertificationId.Value && !t.DeleteMark) .Select(t => t.Id) .ToListAsync(); if (teamIds.Any()) { // 从所有相关团队中移除这些成员 await _db.Updateable() .SetColumns(tm => new TeamMember { DeleteMark = true, UpdatedTime = DateTime.Now, UpdatedUserId = _authManager.UserId }, appendColumnsByDataFilter: true) .Where(tm => ids.Contains(tm.UserId) && teamIds.Contains(tm.TeamId) && !tm.DeleteMark) .ExecuteCommandAsync(); } } } /// /// 变更成员状态 /// /// /// [DisplayName("变更成员状态")] [HttpPost("member/status")] public async Task ChangeMemberStatus(ChangeMemberStatusInput input) { var entity = await _appUserRep.GetByIdAsync(input.Id); if (entity == null || entity.DeleteMark) throw Oops.Bah("成员不存在"); await CheckPermission(entity); entity.Status = input.Status; entity.UpdatedUserId = _authManager.UserId; entity.UpdatedTime = DateTime.Now; await _appUserRep.UpdateAsync(entity); } #endregion }