TeamMemberAppService.cs 13.3 KB
using FoodLabeling.Application.Helpers;
using FoodLabeling.Application.Contracts.Dtos.Common;
using FoodLabeling.Application.Contracts.Dtos.TeamMember;
using FoodLabeling.Application.Contracts.IServices;
using FoodLabeling.Application.Services.DbModels;
using FoodLabeling.Domain.Entities;
using SqlSugar;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Guids;
using Yi.Framework.Rbac.Domain.Entities;
using Yi.Framework.Rbac.Domain.Managers;
using Yi.Framework.SqlSugarCore.Abstractions;

namespace FoodLabeling.Application.Services;

/// <summary>
/// 成员(Team Member/Manager)服务,对外仅在 food-labeling-us 暴露
/// </summary>
public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
{
    private readonly ISqlSugarRepository<UserAggregateRoot, Guid> _userRepository;
    private readonly UserManager _userManager;
    private readonly ISqlSugarDbContext _dbContext;
    private readonly IGuidGenerator _guidGenerator;

    public TeamMemberAppService(
        ISqlSugarRepository<UserAggregateRoot, Guid> userRepository,
        UserManager userManager,
        ISqlSugarDbContext dbContext,
        IGuidGenerator guidGenerator)
    {
        _userRepository = userRepository;
        _userManager = userManager;
        _dbContext = dbContext;
        _guidGenerator = guidGenerator;
    }

    /// <summary>
    /// 成员分页列表(含角色与已分配门店)
    /// </summary>
    public async Task<PagedResultWithPageDto<TeamMemberGetListOutputDto>> GetListAsync(TeamMemberGetListInputVo input)
    {
        var pageIndex = PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount);
        var pageSize = input.MaxResultCount;
        var keyword = input.Keyword?.Trim();

        RefAsync<int> total = 0;

        // 先按 user 表筛选分页,再批量补齐角色与门店
        var users = await _userRepository._DbQueryable
            .Where(u => !u.IsDeleted)
            .WhereIF(!string.IsNullOrWhiteSpace(keyword),
                u => (u.Name != null && u.Name.Contains(keyword!)) ||
                     u.UserName.Contains(keyword!) ||
                     (u.Email != null && u.Email.Contains(keyword!)) ||
                     (u.Phone != null && u.Phone.ToString()!.Contains(keyword!)))
            .WhereIF(input.State != null, u => u.State == input.State)
            .OrderByIF(!string.IsNullOrWhiteSpace(input.Sorting), input.Sorting!)
            .OrderByDescending(u => u.CreationTime)
            .ToPageListAsync(input.SkipCount, input.MaxResultCount, total);

        var userIds = users.Select(x => x.Id).ToList();
        var userIdStrings = userIds.Select(x => x.ToString()).ToList();

        // user-role: 仅取第一个角色(原型表格展示单角色)
        var userRolePairs = await _dbContext.SqlSugarClient.Queryable<UserRoleEntity, RoleAggregateRoot>((ur, r) => ur.RoleId == r.Id)
            .Where(ur => userIds.Contains(ur.UserId))
            .Select((ur, r) => new { ur.UserId, r.Id, r.RoleName })
            .ToListAsync();

        var roleMap = userRolePairs
            .GroupBy(x => x.UserId)
            .ToDictionary(g => g.Key, g => g.FirstOrDefault());

        // user-location
        var userLocations = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>()
            .Where(x => !x.IsDeleted)
            .WhereIF(!string.IsNullOrWhiteSpace(input.LocationId), x => x.LocationId == input.LocationId)
            .Where(x => userIdStrings.Contains(x.UserId))
            .ToListAsync();

        // 如果按 LocationId 过滤,需要反向过滤掉无关联的 user
        if (!string.IsNullOrWhiteSpace(input.LocationId))
        {
            var allowedUserIds = userLocations.Select(x => x.UserId).ToHashSet();
            users = users.Where(u => allowedUserIds.Contains(u.Id.ToString())).ToList();
        }

        var locationIds = userLocations.Select(x => x.LocationId).Distinct().ToList();
        var locations = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>()
            .Where(x => !x.IsDeleted)
            .WhereIF(locationIds.Count > 0, x => locationIds.Contains(x.Id.ToString()))
            .Select(x => new { x.Id, x.LocationCode, x.LocationName })
            .ToListAsync();
        var locationMap = locations.ToDictionary(x => x.Id.ToString(), x => x);

        var assignedMap = userLocations
            .GroupBy(x => x.UserId)
            .ToDictionary(
                g => g.Key,
                g => g.Select(x =>
                {
                    if (locationMap.TryGetValue(x.LocationId, out var loc))
                    {
                        return new TeamMemberAssignedLocationDto
                        {
                            Id = loc.Id.ToString(),
                            LocationCode = loc.LocationCode,
                            LocationName = loc.LocationName
                        };
                    }
                    return null;
                }).Where(x => x != null).Cast<TeamMemberAssignedLocationDto>().ToList());

        var items = users.Select(u =>
        {
            roleMap.TryGetValue(u.Id, out var role);
            assignedMap.TryGetValue(u.Id.ToString(), out var assigned);

            return new TeamMemberGetListOutputDto
            {
                Id = u.Id,
                FullName = u.Name ?? string.Empty,
                UserName = u.UserName,
                Email = u.Email,
                Phone = u.Phone,
                State = u.State,
                RoleId = role?.Id,
                RoleName = role?.RoleName,
                AssignedLocations = assigned ?? new List<TeamMemberAssignedLocationDto>()
            };
        }).ToList();

        var totalCount = (long)total;
        return new PagedResultWithPageDto<TeamMemberGetListOutputDto>
        {
            PageIndex = pageIndex,
            PageSize = pageSize,
            TotalCount = totalCount,
            TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize),
            Items = items
        };
    }

    /// <summary>
    /// 成员详情(带门店ID列表)
    /// </summary>
    public async Task<TeamMemberGetOutputDto> GetAsync(Guid id)
    {
        var user = await _userRepository.GetByIdAsync(id);
        if (user is null || user.IsDeleted)
        {
            throw new UserFriendlyException("成员不存在");
        }

        var userIdString = id.ToString();
        var links = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>()
            .Where(x => !x.IsDeleted && x.UserId == userIdString)
            .ToListAsync();

        var locationIds = links.Select(x => x.LocationId).Distinct().ToList();
        var locations = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>()
            .Where(x => !x.IsDeleted)
            .WhereIF(locationIds.Count > 0, x => locationIds.Contains(x.Id.ToString()))
            .Select(x => new { x.Id, x.LocationCode, x.LocationName })
            .ToListAsync();

        var assigned = locations.Select(x => new TeamMemberAssignedLocationDto
        {
            Id = x.Id.ToString(),
            LocationCode = x.LocationCode,
            LocationName = x.LocationName
        }).ToList();

        var role = await _dbContext.SqlSugarClient.Queryable<UserRoleEntity>().FirstAsync(x => x.UserId == id);

        return new TeamMemberGetOutputDto
        {
            Id = user.Id,
            FullName = user.Name ?? string.Empty,
            UserName = user.UserName,
            Email = user.Email,
            Phone = user.Phone,
            State = user.State,
            RoleId = role?.RoleId,
            LocationIds = locationIds,
            AssignedLocations = assigned
        };
    }

    /// <summary>
    /// 新增成员(同步设置角色与门店)
    /// </summary>
    public async Task<TeamMemberGetOutputDto> CreateAsync(TeamMemberCreateInputVo input)
    {
        if (input.LocationIds is null || input.LocationIds.Count == 0)
        {
            throw new UserFriendlyException("成员必须至少分配一个门店");
        }

        var user = new UserAggregateRoot(input.UserName.Trim(), input.Password, input.Phone, input.FullName.Trim())
        {
            Name = input.FullName.Trim(),
            Email = input.Email?.Trim(),
            State = input.State
        };

        EntityHelper.TrySetId(user, _guidGenerator.Create);
        user.BuildPassword();

        await _userManager.CreateAsync(user);

        if (input.RoleId != null)
        {
            await _userManager.GiveUserSetRoleAsync(new List<Guid> { user.Id }, new List<Guid> { input.RoleId.Value });
        }

        await UpsertUserLocationsAsync(user.Id, input.LocationIds);

        return await GetAsync(user.Id);
    }

    /// <summary>
    /// 编辑成员(同步设置角色与门店)
    /// </summary>
    public async Task<TeamMemberGetOutputDto> UpdateAsync(Guid id, TeamMemberUpdateInputVo input)
    {
        if (input.LocationIds is null || input.LocationIds.Count == 0)
        {
            throw new UserFriendlyException("成员必须至少分配一个门店");
        }

        var user = await _userRepository.GetByIdAsync(id);
        if (user is null || user.IsDeleted)
        {
            throw new UserFriendlyException("成员不存在");
        }

        user.Name = input.FullName.Trim();
        user.UserName = input.UserName.Trim();
        user.Email = input.Email?.Trim();
        user.Phone = input.Phone;
        user.State = input.State;

        if (!string.IsNullOrWhiteSpace(input.Password))
        {
            user.EncryPassword.Password = input.Password;
            user.BuildPassword();
        }

        await _userRepository.UpdateAsync(user);

        // 角色:覆盖式设置(只保留一个)
        if (input.RoleId != null)
        {
            await _userManager.GiveUserSetRoleAsync(new List<Guid> { id }, new List<Guid> { input.RoleId.Value });
        }
        else
        {
            await _userManager.GiveUserSetRoleAsync(new List<Guid> { id }, new List<Guid>());
        }

        await UpsertUserLocationsAsync(id, input.LocationIds);

        return await GetAsync(id);
    }

    /// <summary>
    /// 删除成员(逻辑删除 user;并逻辑删除关联表)
    /// </summary>
    public async Task DeleteAsync(Guid id)
    {
        var user = await _userRepository.GetByIdAsync(id);
        if (user is null || user.IsDeleted)
        {
            return;
        }

        user.IsDeleted = true;
        await _userRepository.UpdateAsync(user);

        var userIdString = id.ToString();
        var currentUserId = CurrentUser?.Id?.ToString();
        await _dbContext.SqlSugarClient.Updateable<UserLocationDbEntity>()
            .SetColumns(x => new UserLocationDbEntity
            {
                IsDeleted = true,
                LastModificationTime = DateTime.Now,
                LastModifierId = currentUserId
            })
            .Where(x => x.UserId == userIdString && !x.IsDeleted)
            .ExecuteCommandAsync();
    }

    private async Task UpsertUserLocationsAsync(Guid userId, List<string> locationIds)
    {
        var now = DateTime.Now;
        var userIdString = userId.ToString();
        var wanted = locationIds.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList();
        var currentUserId = CurrentUser?.Id?.ToString();

        // 校验门店存在且未删除
        var validCount = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>()
            .Where(x => !x.IsDeleted)
            .Where(x => wanted.Contains(x.Id.ToString()))
            .CountAsync();
        if (validCount != wanted.Count)
        {
            throw new UserFriendlyException("存在无效门店,请刷新后重试");
        }

        var existing = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>()
            .Where(x => x.UserId == userIdString)
            .ToListAsync();

        var existingActive = existing.Where(x => !x.IsDeleted).ToList();
        var existingActiveSet = existingActive.Select(x => x.LocationId).ToHashSet();

        // 需要删除的(逻辑删除)
        var toDelete = existingActive.Where(x => !wanted.Contains(x.LocationId)).ToList();
        if (toDelete.Count > 0)
        {
            var ids = toDelete.Select(x => x.Id).ToList();
            await _dbContext.SqlSugarClient.Updateable<UserLocationDbEntity>()
                .SetColumns(x => new UserLocationDbEntity
                {
                    IsDeleted = true,
                    LastModificationTime = now,
                    LastModifierId = currentUserId
                })
                .Where(x => ids.Contains(x.Id))
                .ExecuteCommandAsync();
        }

        // 需要新增的
        var toInsert = wanted.Where(x => !existingActiveSet.Contains(x)).ToList();
        if (toInsert.Count > 0)
        {
            var rows = toInsert.Select(locationId => new UserLocationDbEntity
            {
                Id = _guidGenerator.Create().ToString(),
                IsDeleted = false,
                CreationTime = now,
                CreatorId = currentUserId,
                UserId = userIdString,
                LocationId = locationId,
                ConcurrencyStamp = string.Empty
            }).ToList();

            await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
        }
    }
}