diff --git a/label-template-template-1773988794039 (1).json b/label-template-template-1773988794039 (1).json
new file mode 100644
index 0000000..bbbacba
--- /dev/null
+++ b/label-template-template-1773988794039 (1).json
@@ -0,0 +1,218 @@
+{
+ "id": "template-1773988794039",
+ "name": "未命名模板",
+ "labelType": "PRICE",
+ "unit": "inch",
+ "width": 4,
+ "height": 6,
+ "appliedLocation": "ALL",
+ "showRuler": true,
+ "showGrid": true,
+ "elements": [
+ {
+ "id": "el-1773989351080-vqc03nr",
+ "type": "IMAGE",
+ "x": 32,
+ "y": 24,
+ "width": 60,
+ "height": 60,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "src": "",
+ "scaleMode": "contain"
+ }
+ },
+ {
+ "id": "el-1773989452538-0ejrxoe",
+ "type": "TEXT_STATIC",
+ "x": 32,
+ "y": 104,
+ "width": 120,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "text": "文本",
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "fontWeight": "normal",
+ "textAlign": "left"
+ }
+ },
+ {
+ "id": "el-1773989466493-ibbroio",
+ "type": "QRCODE",
+ "x": 32,
+ "y": 136,
+ "width": 80,
+ "height": 80,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "data": "https://example.com",
+ "errorLevel": "M"
+ }
+ },
+ {
+ "id": "el-1773989469008-f1l39qj",
+ "type": "BARCODE",
+ "x": 0,
+ "y": 224,
+ "width": 160,
+ "height": 48,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "barcodeType": "CODE128",
+ "data": "123456789",
+ "showText": true,
+ "orientation": "horizontal"
+ }
+ },
+ {
+ "id": "el-1773989473436-j7fdeh2",
+ "type": "BLANK",
+ "x": 32,
+ "y": 288,
+ "width": 48,
+ "height": 32,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {}
+ },
+ {
+ "id": "el-1773989483341-ifwcyjj",
+ "type": "TEXT_PRICE",
+ "x": 152,
+ "y": 24,
+ "width": 80,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "text": "0.00",
+ "prefix": "¥",
+ "decimal": 2,
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "fontWeight": "bold",
+ "textAlign": "right"
+ }
+ },
+ {
+ "id": "el-1773989498031-e4d61j8",
+ "type": "IMAGE",
+ "x": 192,
+ "y": 56,
+ "width": 60,
+ "height": 60,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "src": "",
+ "scaleMode": "contain"
+ }
+ },
+ {
+ "id": "el-1773989505076-1lxccx7",
+ "type": "TEXT_PRODUCT",
+ "x": 200,
+ "y": 136,
+ "width": 120,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "text": "商品名",
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "fontWeight": "normal",
+ "textAlign": "left"
+ }
+ },
+ {
+ "id": "el-1773989509805-ax3392v",
+ "type": "TEXT_STATIC",
+ "x": 192,
+ "y": 160,
+ "width": 120,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "text": "文本",
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "fontWeight": "normal",
+ "textAlign": "left"
+ }
+ },
+ {
+ "id": "el-1773989512993-xt8bg7q",
+ "type": "QRCODE",
+ "x": 184,
+ "y": 184,
+ "width": 80,
+ "height": 80,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "data": "https://example.com",
+ "errorLevel": "M"
+ }
+ },
+ {
+ "id": "el-1773989525383-eji8p2s",
+ "type": "BARCODE",
+ "x": 0,
+ "y": 288,
+ "width": 160,
+ "height": 48,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "barcodeType": "CODE128",
+ "data": "123456789",
+ "showText": true,
+ "orientation": "horizontal"
+ }
+ },
+ {
+ "id": "el-1773989540159-dr2avdf",
+ "type": "NUTRITION",
+ "x": 184,
+ "y": 280,
+ "width": 200,
+ "height": 120,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "calories": 120,
+ "fat": "5g",
+ "protein": "3g",
+ "carbs": "10g",
+ "layout": "standard"
+ }
+ },
+ {
+ "id": "el-1773989549679-mcxrdnw",
+ "type": "TEXT_PRICE",
+ "x": 24,
+ "y": 352,
+ "width": 80,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "text": "0.00",
+ "prefix": "¥",
+ "decimal": 2,
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "fontWeight": "bold",
+ "textAlign": "right"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleGetOutputDto.cs
index 877e1b0..c59c753 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleGetOutputDto.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleGetOutputDto.cs
@@ -5,5 +5,9 @@ namespace FoodLabeling.Application.Contracts.Dtos.RbacRole;
///
public class RbacRoleGetOutputDto : RbacRoleGetListOutputDto
{
+ ///
+ /// 该角色已分配的菜单权限ID列表
+ ///
+ public List MenuIds { get; set; } = new();
}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberAssignedLocationDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberAssignedLocationDto.cs
new file mode 100644
index 0000000..062a805
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberAssignedLocationDto.cs
@@ -0,0 +1,11 @@
+namespace FoodLabeling.Application.Contracts.Dtos.TeamMember;
+
+public class TeamMemberAssignedLocationDto
+{
+ public string Id { get; set; } = string.Empty;
+
+ public string LocationCode { get; set; } = string.Empty;
+
+ public string LocationName { get; set; } = string.Empty;
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs
new file mode 100644
index 0000000..aedc9e6
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs
@@ -0,0 +1,27 @@
+namespace FoodLabeling.Application.Contracts.Dtos.TeamMember;
+
+public class TeamMemberCreateInputVo
+{
+ public string FullName { get; set; } = string.Empty;
+
+ ///
+ /// 登录账号(建议用邮箱或自定义用户名)
+ ///
+ public string UserName { get; set; } = string.Empty;
+
+ public string Password { get; set; } = string.Empty;
+
+ public string? Email { get; set; }
+
+ public long? Phone { get; set; }
+
+ public Guid? RoleId { get; set; }
+
+ ///
+ /// 关联门店(至少1个)
+ ///
+ public List LocationIds { get; set; } = new();
+
+ public bool State { get; set; } = true;
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListInputVo.cs
new file mode 100644
index 0000000..55f2c29
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListInputVo.cs
@@ -0,0 +1,30 @@
+using Volo.Abp.Application.Dtos;
+
+namespace FoodLabeling.Application.Contracts.Dtos.TeamMember;
+
+///
+/// 成员分页查询入参
+///
+public class TeamMemberGetListInputVo : PagedAndSortedResultRequestDto
+{
+ ///
+ /// 关键字(姓名/用户名/邮箱/电话)
+ ///
+ public string? Keyword { get; set; }
+
+ ///
+ /// 角色ID(可选)
+ ///
+ public Guid? RoleId { get; set; }
+
+ ///
+ /// 门店ID(可选)
+ ///
+ public string? LocationId { get; set; }
+
+ ///
+ /// 启用状态(可选)
+ ///
+ public bool? State { get; set; }
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs
new file mode 100644
index 0000000..90a383c
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs
@@ -0,0 +1,26 @@
+namespace FoodLabeling.Application.Contracts.Dtos.TeamMember;
+
+public class TeamMemberGetListOutputDto
+{
+ public Guid Id { get; set; }
+
+ public string FullName { get; set; } = string.Empty;
+
+ public string UserName { get; set; } = string.Empty;
+
+ public string? Email { get; set; }
+
+ public long? Phone { get; set; }
+
+ public bool State { get; set; }
+
+ ///
+ /// 角色(当前仅返回第一个)
+ ///
+ public Guid? RoleId { get; set; }
+
+ public string? RoleName { get; set; }
+
+ public List AssignedLocations { get; set; } = new();
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetOutputDto.cs
new file mode 100644
index 0000000..9daa71a
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetOutputDto.cs
@@ -0,0 +1,23 @@
+namespace FoodLabeling.Application.Contracts.Dtos.TeamMember;
+
+public class TeamMemberGetOutputDto
+{
+ public Guid Id { get; set; }
+
+ public string FullName { get; set; } = string.Empty;
+
+ public string UserName { get; set; } = string.Empty;
+
+ public string? Email { get; set; }
+
+ public long? Phone { get; set; }
+
+ public bool State { get; set; }
+
+ public Guid? RoleId { get; set; }
+
+ public List LocationIds { get; set; } = new();
+
+ public List AssignedLocations { get; set; } = new();
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs
new file mode 100644
index 0000000..f0e1e7d
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs
@@ -0,0 +1,24 @@
+namespace FoodLabeling.Application.Contracts.Dtos.TeamMember;
+
+public class TeamMemberUpdateInputVo
+{
+ public string FullName { get; set; } = string.Empty;
+
+ public string UserName { get; set; } = string.Empty;
+
+ ///
+ /// 为空表示不改密码
+ ///
+ public string? Password { get; set; }
+
+ public string? Email { get; set; }
+
+ public long? Phone { get; set; }
+
+ public Guid? RoleId { get; set; }
+
+ public List LocationIds { get; set; } = new();
+
+ public bool State { get; set; } = true;
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITeamMemberAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITeamMemberAppService.cs
new file mode 100644
index 0000000..72f4dfe
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITeamMemberAppService.cs
@@ -0,0 +1,18 @@
+using FoodLabeling.Application.Contracts.Dtos.Common;
+using FoodLabeling.Application.Contracts.Dtos.TeamMember;
+
+namespace FoodLabeling.Application.Contracts.IServices;
+
+public interface ITeamMemberAppService
+{
+ Task> GetListAsync(TeamMemberGetListInputVo input);
+
+ Task GetAsync(Guid id);
+
+ Task CreateAsync(TeamMemberCreateInputVo input);
+
+ Task UpdateAsync(Guid id, TeamMemberUpdateInputVo input);
+
+ Task DeleteAsync(Guid id);
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/RoleMenuDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/RoleMenuDbEntity.cs
new file mode 100644
index 0000000..30e81d7
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/RoleMenuDbEntity.cs
@@ -0,0 +1,18 @@
+using SqlSugar;
+
+namespace FoodLabeling.Application.Services.DbModels;
+
+///
+/// rolemenu 表映射(兼容字符串类型 RoleId/MenuId)
+///
+[SugarTable("rolemenu")]
+public class RoleMenuDbEntity
+{
+ [SugarColumn(IsPrimaryKey = true)]
+ public string Id { get; set; } = string.Empty;
+
+ public string RoleId { get; set; } = string.Empty;
+
+ public string MenuId { get; set; } = string.Empty;
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/UserLocationDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/UserLocationDbEntity.cs
new file mode 100644
index 0000000..32a7db0
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/UserLocationDbEntity.cs
@@ -0,0 +1,30 @@
+using SqlSugar;
+
+namespace FoodLabeling.Application.Services.DbModels;
+
+///
+/// userlocation 表映射(成员-门店关联)
+///
+[SugarTable("userlocation")]
+public class UserLocationDbEntity
+{
+ [SugarColumn(IsPrimaryKey = true)]
+ public string Id { get; set; } = string.Empty;
+
+ public bool IsDeleted { get; set; }
+
+ public DateTime CreationTime { get; set; }
+
+ public string? CreatorId { get; set; }
+
+ public string? LastModifierId { get; set; }
+
+ public DateTime? LastModificationTime { get; set; }
+
+ public string UserId { get; set; } = string.Empty;
+
+ public string LocationId { get; set; } = string.Empty;
+
+ public string? ConcurrencyStamp { get; set; }
+}
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleAppService.cs
index 7a3c28a..012867b 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleAppService.cs
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleAppService.cs
@@ -1,6 +1,7 @@
using FoodLabeling.Application.Contracts.Dtos.RbacRole;
using FoodLabeling.Application.Contracts.Dtos.Common;
using FoodLabeling.Application.Contracts.IServices;
+using FoodLabeling.Application.Services.DbModels;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using Volo.Abp;
@@ -17,17 +18,20 @@ namespace FoodLabeling.Application.Services;
///
public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
{
+ private readonly ISqlSugarDbContext _dbContext;
private readonly ISqlSugarRepository _roleRepository;
private readonly ISqlSugarRepository _roleMenuRepository;
private readonly ISqlSugarRepository _roleDeptRepository;
private readonly ISqlSugarRepository _userRoleRepository;
public RbacRoleAppService(
+ ISqlSugarDbContext dbContext,
ISqlSugarRepository roleRepository,
ISqlSugarRepository roleMenuRepository,
ISqlSugarRepository roleDeptRepository,
ISqlSugarRepository userRoleRepository)
{
+ _dbContext = dbContext;
_roleRepository = roleRepository;
_roleMenuRepository = roleMenuRepository;
_roleDeptRepository = roleDeptRepository;
@@ -90,6 +94,11 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
throw new UserFriendlyException("角色不存在");
}
+ var menuIds = await _dbContext.SqlSugarClient.Queryable()
+ .Where(x => x.RoleId == id.ToString())
+ .Select(x => x.MenuId)
+ .ToListAsync();
+
return new RbacRoleGetOutputDto
{
Id = entity.Id,
@@ -98,7 +107,8 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
Remark = entity.Remark,
DataScope = (int)entity.DataScope,
State = entity.State,
- OrderNum = entity.OrderNum
+ OrderNum = entity.OrderNum,
+ MenuIds = menuIds
};
}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs
new file mode 100644
index 0000000..4cfe8bd
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs
@@ -0,0 +1,355 @@
+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;
+
+///
+/// 成员(Team Member/Manager)服务,对外仅在 food-labeling-us 暴露
+///
+public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
+{
+ private readonly ISqlSugarRepository _userRepository;
+ private readonly UserManager _userManager;
+ private readonly ISqlSugarDbContext _dbContext;
+ private readonly IGuidGenerator _guidGenerator;
+
+ public TeamMemberAppService(
+ ISqlSugarRepository userRepository,
+ UserManager userManager,
+ ISqlSugarDbContext dbContext,
+ IGuidGenerator guidGenerator)
+ {
+ _userRepository = userRepository;
+ _userManager = userManager;
+ _dbContext = dbContext;
+ _guidGenerator = guidGenerator;
+ }
+
+ ///
+ /// 成员分页列表(含角色与已分配门店)
+ ///
+ public async Task> GetListAsync(TeamMemberGetListInputVo input)
+ {
+ var pageIndex = input.SkipCount / input.MaxResultCount + 1;
+ var pageSize = input.MaxResultCount;
+ var keyword = input.Keyword?.Trim();
+
+ RefAsync 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((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()
+ .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()
+ .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 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()
+ };
+ }).ToList();
+
+ var totalCount = (long)total;
+ return new PagedResultWithPageDto
+ {
+ PageIndex = pageIndex,
+ PageSize = pageSize,
+ TotalCount = totalCount,
+ TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize),
+ Items = items
+ };
+ }
+
+ ///
+ /// 成员详情(带门店ID列表)
+ ///
+ 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);
+
+ 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
+ };
+ }
+
+ ///
+ /// 新增成员(同步设置角色与门店)
+ ///
+ public async Task 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 { user.Id }, new List { input.RoleId.Value });
+ }
+
+ await UpsertUserLocationsAsync(user.Id, input.LocationIds);
+
+ return await GetAsync(user.Id);
+ }
+
+ ///
+ /// 编辑成员(同步设置角色与门店)
+ ///
+ public async Task 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 { id }, new List { input.RoleId.Value });
+ }
+ else
+ {
+ await _userManager.GiveUserSetRoleAsync(new List { id }, new List());
+ }
+
+ await UpsertUserLocationsAsync(id, input.LocationIds);
+
+ return await GetAsync(id);
+ }
+
+ ///
+ /// 删除成员(逻辑删除 user;并逻辑删除关联表)
+ ///
+ 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();
+ }
+
+ 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();
+ }
+ }
+}
+