using System.IO;
using FoodLabeling.Application.Contracts.Dtos.Common;
using FoodLabeling.Application.Contracts.Dtos.TeamMember;
using FoodLabeling.Application.Contracts.IServices;
using FoodLabeling.Application.Helpers;
using FoodLabeling.Application.Options;
using FoodLabeling.Application.Services.DbModels;
using FoodLabeling.Domain.Entities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
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.Entities.ValueObjects;
using Yi.Framework.Rbac.Domain.Helpers;
using Yi.Framework.Rbac.Domain.Managers;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace FoodLabeling.Application.Services;
///
/// 成员(Team Member)服务,对外仅在 food-labeling-us 暴露
///
public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
{
private readonly ISqlSugarRepository _userRepository;
private readonly UserManager _userManager;
private readonly ISqlSugarDbContext _dbContext;
private readonly IGuidGenerator _guidGenerator;
private readonly IOptionsSnapshot _batchImportOptions;
public TeamMemberAppService(
ISqlSugarRepository userRepository,
UserManager userManager,
ISqlSugarDbContext dbContext,
IGuidGenerator guidGenerator,
IOptionsSnapshot batchImportOptions)
{
_userRepository = userRepository;
_userManager = userManager;
_dbContext = dbContext;
_guidGenerator = guidGenerator;
_batchImportOptions = batchImportOptions;
}
///
public async Task> GetListAsync(TeamMemberGetListInputVo input)
{
var pageIndex = PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount);
var pageSize = input.MaxResultCount;
RefAsync total = 0;
var scopeLocationIds = await LocationScopeBindingHelper.ResolveFilteredLocationIdsForListAsync(
_dbContext.SqlSugarClient, input.PartnerId, input.GroupId, input.LocationId);
var query = await BuildFilteredUserQueryAsync(input, scopeLocationIds);
var users = await query
.OrderByIF(!string.IsNullOrWhiteSpace(input.Sorting), input.Sorting!)
.OrderByDescending(u => u.CreationTime)
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
var items = await MapUsersToOutputAsync(
users,
scopeLocationIds,
restrictAssignedLocationsToFilter: scopeLocationIds is not null);
var totalCount = (long)total;
return new PagedResultWithPageDto
{
PageIndex = pageIndex,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize),
Items = items
};
}
///
public async Task 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()
.Where(x => !x.IsDeleted && x.UserId == userIdString)
.ToListAsync();
var locationIds = links.Select(x => x.LocationId).Distinct().ToList();
var locations = await _dbContext.SqlSugarClient.Queryable()
.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().FirstAsync(x => x.UserId == id);
var partnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(
_dbContext.SqlSugarClient, locationIds);
var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(
_dbContext.SqlSugarClient, locationIds);
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,
PartnerIds = partnerIds,
RegionIds = regionIds,
GroupIds = regionIds,
LocationIds = locationIds,
AssignedLocations = assigned
};
}
///
public async Task CreateAsync(TeamMemberCreateInputVo input)
{
var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input);
var user = new UserAggregateRoot
{
UserName = input.UserName.Trim(),
Name = input.FullName.Trim(),
Nick = input.FullName.Trim(),
Email = input.Email?.Trim(),
Phone = input.Phone,
State = input.State,
EncryPassword = new EncryPasswordValueObject(input.Password.Trim())
};
EntityHelper.TrySetId(user, _guidGenerator.Create);
user.BuildPassword();
await _userManager.CreateAsync(user);
if (input.RoleId != null)
{
await _userManager.GiveUserSetRoleAsync(new List { user.Id }, new List { input.RoleId.Value });
}
await UpsertUserLocationsAsync(user.Id, mergedLocationIds);
return await GetAsync(user.Id);
}
///
public async Task UpdateAsync(Guid id, TeamMemberUpdateInputVo input)
{
var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input);
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;
var passwordChanged = false;
if (!string.IsNullOrWhiteSpace(input.Password))
{
UserPasswordHelper.ApplyPlainPassword(user, input.Password);
passwordChanged = true;
}
await _userRepository.UpdateAsync(user);
if (passwordChanged)
{
await UserPasswordHelper.EnsurePasswordColumnsPersistedAsync(
_userRepository,
user.Id,
user.EncryPassword.Password,
user.EncryPassword.Salt);
}
if (input.RoleId != null)
{
await _userManager.GiveUserSetRoleAsync(new List { id }, new List { input.RoleId.Value });
}
else
{
await _userManager.GiveUserSetRoleAsync(new List { id }, new List());
}
await UpsertUserLocationsAsync(id, mergedLocationIds);
return await GetAsync(id);
}
///
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()
.SetColumns(x => new UserLocationDbEntity
{
IsDeleted = true,
LastModificationTime = DateTime.Now,
LastModifierId = currentUserId
})
.Where(x => x.UserId == userIdString && !x.IsDeleted)
.ExecuteCommandAsync();
}
///
public Task DownloadTeamMemberImportTemplateAsync()
{
var opt = _batchImportOptions.Value;
var dir = opt.TemplateDirectory?.Trim();
if (string.IsNullOrWhiteSpace(dir))
{
throw new UserFriendlyException("未配置批量导入模板目录 FoodLabeling:BatchImport:TemplateDirectory");
}
var fileName = opt.TeamMemberTemplateFileName?.Trim();
if (string.IsNullOrWhiteSpace(fileName))
{
throw new UserFriendlyException("未配置模板文件名 FoodLabeling:BatchImport:TeamMemberTemplateFileName");
}
var fullPath = Path.Combine(dir, fileName);
if (!File.Exists(fullPath))
{
throw new UserFriendlyException($"模板文件不存在:{fullPath}");
}
var stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
const string contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
return Task.FromResult(new FileStreamResult(stream, contentType)
{
FileDownloadName = fileName
});
}
///
public async Task ExportTeamMembersPdfAsync([FromQuery] TeamMemberGetListInputVo input)
{
QuestPDF.Settings.License = LicenseType.Community;
var scopeLocationIds = await LocationScopeBindingHelper.ResolveFilteredLocationIdsForListAsync(
_dbContext.SqlSugarClient, input.PartnerId, input.GroupId, input.LocationId);
var query = await BuildFilteredUserQueryAsync(input, scopeLocationIds);
var users = await query
.OrderByIF(!string.IsNullOrWhiteSpace(input.Sorting), input.Sorting!)
.OrderByDescending(u => u.CreationTime)
.ToListAsync();
var rows = await MapUsersToOutputAsync(
users,
scopeLocationIds,
restrictAssignedLocationsToFilter: scopeLocationIds is not null);
var fileName = $"team-members_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf";
var document = Document.Create(container =>
{
container.Page(page =>
{
page.Margin(22);
page.DefaultTextStyle(x => x.FontSize(8));
page.Header().Text("Team Members").SemiBold().FontSize(16);
page.Content().PaddingTop(8).Table(table =>
{
table.ColumnsDefinition(c =>
{
c.RelativeColumn(1.4f);
c.RelativeColumn(1.6f);
c.RelativeColumn(1.1f);
c.RelativeColumn(1.1f);
c.RelativeColumn(2.2f);
c.RelativeColumn(0.7f);
});
static IContainer CellHeader(IContainer c) =>
c.Background(Colors.Grey.Lighten3).Padding(4).DefaultTextStyle(x => x.SemiBold());
table.Cell().Element(CellHeader).Text("Name");
table.Cell().Element(CellHeader).Text("Email");
table.Cell().Element(CellHeader).Text("Phone");
table.Cell().Element(CellHeader).Text("Role");
table.Cell().Element(CellHeader).Text("Assigned Locations");
table.Cell().Element(CellHeader).Text("Status");
foreach (var e in rows)
{
var locText = e.AssignedLocations.Count == 0
? "无"
: string.Join("; ",
e.AssignedLocations.Select(a =>
$"{a.LocationCode} - {a.LocationName}"));
var status = e.State ? "Active" : "Inactive";
table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
.Text(e.FullName ?? string.Empty);
table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
.Text(e.Email ?? "无");
table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
.Text(e.Phone?.ToString() ?? "无");
table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
.Text(string.IsNullOrWhiteSpace(e.RoleName) ? "无" : e.RoleName);
table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
.Text(locText);
table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
.Text(status);
}
});
});
});
var stream = new MemoryStream();
document.GeneratePdf(stream);
stream.Position = 0;
return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName };
}
///
public async Task ImportTeamMembersBatchAsync(
[FromForm] TeamMemberBatchImportInputVo input)
{
if (input?.File is null || input.File.Length == 0)
{
throw new UserFriendlyException("请上传 Excel 文件(form 字段名:file)");
}
var opt = _batchImportOptions.Value;
if (input.File.Length > opt.MaxUploadBytes)
{
throw new UserFriendlyException($"文件过大,最大允许 {opt.MaxUploadBytes / 1024 / 1024} MB");
}
var ext = Path.GetExtension(input.File.FileName)?.ToLowerInvariant();
if (ext != ".xlsx")
{
throw new UserFriendlyException("仅支持 .xlsx 格式的 Excel 文件");
}
var roleMap = await BuildRoleNameToIdMapAsync();
await using var uploadStream = input.File.OpenReadStream();
var parseErrors = new List();
var rows = TeamMemberBatchExcelHelper.ParseImportWorkbook(
uploadStream,
opt.MaxImportRows <= 0 ? 5000 : opt.MaxImportRows,
roleMap,
opt.TeamMemberImportDefaultPassword?.Trim() ?? string.Empty,
out var headerErrors);
parseErrors.AddRange(headerErrors);
var result = new TeamMemberBatchImportResultDto();
if (rows.Count == 0 && parseErrors.Count > 0)
{
result.Errors = parseErrors;
result.FailCount = parseErrors.Count;
return result;
}
foreach (var (rowNum, vo) in rows)
{
try
{
vo.LocationIds = await ResolveLocationIdsFromImportTokensAsync(vo.LocationIds);
await CreateAsync(vo);
result.SuccessCount++;
}
catch (UserFriendlyException ex)
{
result.FailCount++;
result.Errors.Add(new TeamMemberBatchImportErrorDto
{
RowNumber = rowNum,
UserName = vo.UserName,
Message = ex.Message
});
}
}
result.Errors.InsertRange(0, parseErrors);
return result;
}
///
public async Task UpdateTeamMembersBulkAsync(
[FromBody] TeamMemberBulkUpdateInputVo input)
{
if (input?.Items is null || input.Items.Count == 0)
{
throw new UserFriendlyException("请至少提交一条编辑数据(items 不能为空)");
}
var opt = _batchImportOptions.Value;
var maxItems = opt.MaxBulkUpdateItems <= 0 ? 500 : opt.MaxBulkUpdateItems;
if (input.Items.Count > maxItems)
{
throw new UserFriendlyException($"单次批量编辑最多允许 {maxItems} 条,请分批提交");
}
var effectiveCount = input.Items.Count(static x => x is not null && x.Id != Guid.Empty);
if (effectiveCount == 0)
{
throw new UserFriendlyException("没有有效的成员 Id(请为待保存行填写 id)");
}
var result = new TeamMemberBulkUpdateResultDto();
for (var i = 0; i < input.Items.Count; i++)
{
var item = input.Items[i];
if (item is null || item.Id == Guid.Empty)
{
continue;
}
try
{
await UpdateAsync(item.Id, item);
result.SuccessCount++;
}
catch (UserFriendlyException ex)
{
result.FailCount++;
result.Errors.Add(new TeamMemberBulkUpdateErrorDto
{
RowNumber = i + 1,
Id = item.Id,
Message = ex.Message
});
}
}
return result;
}
private async Task> BuildRoleNameToIdMapAsync()
{
var roles = await _dbContext.SqlSugarClient.Queryable()
.Where(r => !r.IsDeleted)
.Select(r => new { r.Id, r.RoleName })
.ToListAsync();
return roles
.Where(r => !string.IsNullOrWhiteSpace(r.RoleName))
.GroupBy(r => TeamMemberBatchExcelHelper.NormalizeRoleKey(r.RoleName!))
.ToDictionary(g => g.Key, g => g.First().Id);
}
private async Task> ResolveLocationIdsFromImportTokensAsync(List tokens)
{
var result = new List();
foreach (var raw in tokens)
{
var s = raw.Trim();
if (string.IsNullOrEmpty(s))
{
continue;
}
var idx = s.IndexOf(" -", StringComparison.Ordinal);
var key = idx > 0 ? s[..idx].Trim() : s.Trim();
if (Guid.TryParse(key, out var gid))
{
var byId = await _dbContext.SqlSugarClient.Queryable()
.Where(x => !x.IsDeleted && x.Id == gid)
.FirstAsync();
if (byId is null)
{
throw new UserFriendlyException($"无效门店 Id:{key}");
}
result.Add(byId.Id.ToString());
continue;
}
var byCode = await _dbContext.SqlSugarClient.Queryable()
.Where(x => !x.IsDeleted && x.LocationCode == key)
.FirstAsync();
if (byCode is null)
{
throw new UserFriendlyException($"未找到门店编码:{key}");
}
result.Add(byCode.Id.ToString());
}
return result.Distinct().ToList();
}
private async Task> BuildFilteredUserQueryAsync(
TeamMemberGetListInputVo input,
List? scopeLocationIds)
{
var keyword = input.Keyword?.Trim();
var query = _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);
if (input.RoleId != null)
{
var userIds = await _dbContext.SqlSugarClient.Queryable()
.Where(ur => ur.RoleId == input.RoleId.Value)
.Select(ur => ur.UserId)
.ToListAsync();
query = query.Where(u => userIds.Contains(u.Id));
}
if (scopeLocationIds is not null)
{
if (scopeLocationIds.Count == 0)
{
query = query.Where(_ => false);
}
else
{
var scopeSet = new HashSet(scopeLocationIds, StringComparer.Ordinal);
var userIdStrs = await _dbContext.SqlSugarClient.Queryable()
.Where(x => !x.IsDeleted && scopeSet.Contains(x.LocationId))
.Select(x => x.UserId)
.ToListAsync();
var allowed = new HashSet(userIdStrs);
query = query.Where(u => allowed.Contains(u.Id.ToString()));
}
}
return query;
}
private async Task> MapUsersToOutputAsync(
List users,
List? scopeLocationIds,
bool restrictAssignedLocationsToFilter)
{
if (users.Count == 0)
{
return new List();
}
var userIds = users.Select(x => x.Id).ToList();
var userIdStrings = userIds.Select(x => x.ToString()).ToList();
var userRolePairs = await _dbContext.SqlSugarClient.Queryable((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());
var userLocQuery = _dbContext.SqlSugarClient.Queryable()
.Where(x => !x.IsDeleted)
.Where(x => userIdStrings.Contains(x.UserId));
if (restrictAssignedLocationsToFilter && scopeLocationIds is { Count: > 0 })
{
var scopeSet = new HashSet(scopeLocationIds, StringComparer.Ordinal);
userLocQuery = userLocQuery.Where(x => scopeSet.Contains(x.LocationId));
}
var userLocations = await userLocQuery.ToListAsync();
var locationIds = userLocations.Select(x => x.LocationId).Distinct().ToList();
var locations = await _dbContext.SqlSugarClient.Queryable()
.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().ToList());
var scopeIdsMap = await BuildTeamMemberScopeIdsMapAsync(assignedMap);
return users.Select(u =>
{
roleMap.TryGetValue(u.Id, out var role);
assignedMap.TryGetValue(u.Id.ToString(), out var assigned);
scopeIdsMap.TryGetValue(u.Id.ToString(), out var scopeIds);
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,
PartnerIds = scopeIds?.PartnerIds ?? new List(),
RegionIds = scopeIds?.RegionIds ?? new List(),
AssignedLocations = assigned ?? new List()
};
}).ToList();
}
private async Task> BuildTeamMemberScopeIdsMapAsync(
Dictionary> assignedMap)
{
var result = new Dictionary(StringComparer.Ordinal);
foreach (var (userId, assigned) in assignedMap)
{
var locationIds = assigned
.Select(x => x.Id)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => x.Trim())
.Distinct(StringComparer.Ordinal)
.ToList();
if (locationIds.Count == 0)
{
result[userId] = new TeamMemberScopeIds();
continue;
}
var partnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(
_dbContext.SqlSugarClient, locationIds);
var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(
_dbContext.SqlSugarClient, locationIds);
result[userId] = new TeamMemberScopeIds
{
PartnerIds = partnerIds,
RegionIds = regionIds
};
}
return result;
}
private sealed class TeamMemberScopeIds
{
public List PartnerIds { get; init; } = new();
public List RegionIds { get; init; } = new();
}
private Task> ResolveTeamMemberLocationIdsForSaveAsync(TeamMemberUpdateInputVo input) =>
ResolveTeamMemberLocationIdsForSaveAsync(new TeamMemberCreateInputVo
{
PartnerId = input.PartnerId,
PartnerIds = input.PartnerIds,
RegionIds = input.RegionIds,
GroupIds = input.GroupIds,
LocationIds = input.LocationIds
});
private async Task> ResolveTeamMemberLocationIdsForSaveAsync(TeamMemberCreateInputVo input)
{
var partnerIds = NormalizePartnerIds(input);
var regionIds = NormalizeRegionIds(input);
var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
_dbContext.SqlSugarClient, partnerIds, regionIds, input.LocationIds);
if (merged.Count == 0)
{
throw new UserFriendlyException("成员必须至少分配一个门店(公司/区域/门店至少选一项)");
}
await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged);
return merged;
}
private static List NormalizePartnerIds(TeamMemberCreateInputVo input)
{
var merged = new HashSet(StringComparer.Ordinal);
if (!string.IsNullOrWhiteSpace(input.PartnerId))
{
merged.Add(input.PartnerId.Trim());
}
foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.PartnerIds))
{
merged.Add(id);
}
return merged.OrderBy(x => x, StringComparer.Ordinal).ToList();
}
private static List NormalizeRegionIds(TeamMemberCreateInputVo input)
{
var merged = new HashSet(StringComparer.Ordinal);
foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.RegionIds))
{
merged.Add(id);
}
foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.GroupIds))
{
merged.Add(id);
}
return merged.OrderBy(x => x, StringComparer.Ordinal).ToList();
}
private async Task UpsertUserLocationsAsync(Guid userId, List 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()
.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()
.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()
.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();
}
}
}