feat(customer): 列表返回 Id 并支持管理员代登客户门户
补充客户分页 Id 字段与 customer-oms-login 接口,修复管理端删除/免密登录;公告通知增加接收人候选项与客户接收类型。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
90f6c25b04
commit
74151b0738
@ -7,6 +7,11 @@ namespace Oms.Application.Dtos.Customers;
|
||||
/// </summary>
|
||||
public class CustomerPaginatedOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键。
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父客户ID,空表示主客户。
|
||||
/// </summary>
|
||||
|
||||
@ -46,10 +46,15 @@ public class NoticeOutput
|
||||
public string ReceiverTypeName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 接收主体 ID 列表。
|
||||
/// 接收主体 ID 列表(持久化为 <c>user.userid</c>)。
|
||||
/// </summary>
|
||||
public List<int> ReceiverIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 表单回显用选中 ID:user 与 <see cref="ReceiverIds"/> 一致;customer 为 <c>t_customer.Id</c>。
|
||||
/// </summary>
|
||||
public List<int> ReceiverSelectionIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 已读人数。
|
||||
/// </summary>
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
namespace Oms.Application.Dtos.System;
|
||||
|
||||
/// <summary>
|
||||
/// 公告接收人候选项。
|
||||
/// </summary>
|
||||
public class NoticeReceiverOptionOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 表单选中值:user 为 <c>user.userid</c>,customer 为 <c>t_customer.Id</c>。
|
||||
/// </summary>
|
||||
public int Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 展示名称。
|
||||
/// </summary>
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 辅助说明(账号、客户编码等)。
|
||||
/// </summary>
|
||||
public string? Extra { get; set; }
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
namespace Oms.Application.Dtos.System;
|
||||
|
||||
/// <summary>
|
||||
/// 公告接收人候选项查询入参。
|
||||
/// </summary>
|
||||
public class NoticeReceiverOptionsInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 接收对象类型:user/customer。
|
||||
/// </summary>
|
||||
public string ReceiverType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 关键字。
|
||||
/// </summary>
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; set; } = 50;
|
||||
}
|
||||
@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Dtos.System;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.CustomersAggregate.Entities;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.Domain.SystemAggregate.Entities;
|
||||
using Oms.Domain.SystemAggregate.Repositories;
|
||||
@ -21,6 +22,21 @@ public class NoticeService : ApplicationService
|
||||
private const string NoticeTypeNotice = "notice";
|
||||
private const string NoticeTypeMessage = "message";
|
||||
private const string ReceiverTypeUser = "user";
|
||||
private const string ReceiverTypeCustomer = "customer";
|
||||
|
||||
private const string InternalUserFilter = """
|
||||
(
|
||||
NOT EXISTS (SELECT 1 FROM `user_role` ur0 WHERE ur0.`UserId` = u.`Id`)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM `user_role` ur
|
||||
INNER JOIN `role` r ON r.`Id` = ur.`RoleId` AND r.`IsDeleted` = 0
|
||||
WHERE ur.`UserId` = u.`Id`
|
||||
AND r.`Code` <> 'Customer'
|
||||
AND r.`Code` NOT LIKE 'CustomerSub\_%'
|
||||
)
|
||||
)
|
||||
""";
|
||||
|
||||
private INoticeRepository NoticeRepository => LazyServiceProvider.GetRequiredService<INoticeRepository>();
|
||||
private INoticeReadRepository NoticeReadRepository => LazyServiceProvider.GetRequiredService<INoticeReadRepository>();
|
||||
@ -30,8 +46,6 @@ public class NoticeService : ApplicationService
|
||||
/// <summary>
|
||||
/// 查询通知列表。
|
||||
/// </summary>
|
||||
/// <param name="input">入参:通知分页查询条件。</param>
|
||||
/// <returns>通知分页结果,包含通知主体、接收范围与已读统计。</returns>
|
||||
[HttpGet("/system/notice/list")]
|
||||
public async Task<OmsPageResult<NoticeOutput>> GetListAsync([FromQuery] NoticeListInput input)
|
||||
{
|
||||
@ -55,20 +69,40 @@ public class NoticeService : ApplicationService
|
||||
x => x.Key,
|
||||
x => (int)x.Select(item => $"{item.ReceiverType}:{item.ReceiverId}").Distinct(StringComparer.Ordinal).Count());
|
||||
|
||||
var selectionIdMap = await BuildReceiverSelectionIdMapAsync(notices);
|
||||
|
||||
return new OmsPageResult<NoticeOutput>
|
||||
{
|
||||
Items = notices.Select(x => MapNoticeOutput(
|
||||
x,
|
||||
readCountMap.TryGetValue(x.Id, out var readCount) ? readCount : 0)).ToList(),
|
||||
readCountMap.TryGetValue(x.Id, out var readCount) ? readCount : 0,
|
||||
selectionIdMap.TryGetValue(x.Id, out var selectionIds) ? selectionIds : [])).ToList(),
|
||||
Total = total
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 接收人候选项(按接收对象类型加载)。
|
||||
/// </summary>
|
||||
[HttpGet("/system/notice/receiver-options")]
|
||||
public async Task<OmsPageResult<NoticeReceiverOptionOutput>> GetReceiverOptionsAsync([FromQuery] NoticeReceiverOptionsInput input)
|
||||
{
|
||||
var receiverType = NormalizeReceiverType(input.ReceiverType);
|
||||
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
|
||||
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
|
||||
var keyword = input.Keyword?.Trim();
|
||||
|
||||
if (receiverType == ReceiverTypeCustomer)
|
||||
{
|
||||
return await QueryCustomerReceiverOptionsAsync(keyword, pageIndex, pageSize);
|
||||
}
|
||||
|
||||
return await QueryUserReceiverOptionsAsync(keyword, pageIndex, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增通知。
|
||||
/// </summary>
|
||||
/// <param name="input">入参:通知新增参数。</param>
|
||||
/// <returns>无返回体,仅完成新增通知。</returns>
|
||||
[HttpPost("/system/notice/create")]
|
||||
public async Task CreateAsync(NoticeCreateInput input)
|
||||
{
|
||||
@ -88,8 +122,6 @@ public class NoticeService : ApplicationService
|
||||
/// <summary>
|
||||
/// 更新通知。
|
||||
/// </summary>
|
||||
/// <param name="input">入参:通知更新参数。</param>
|
||||
/// <returns>无返回体,仅完成更新通知。</returns>
|
||||
[HttpPost("/system/notice/update")]
|
||||
public async Task UpdateAsync(NoticeUpdateInput input)
|
||||
{
|
||||
@ -106,7 +138,7 @@ public class NoticeService : ApplicationService
|
||||
|
||||
var existsReads = await NoticeReadRepository.GetListAsync(x => x.NoticeId == entity.Id);
|
||||
var receiverIdSet = receiverIds.ToHashSet();
|
||||
foreach (var read in existsReads.Where(x => x.ReceiverType != entity.ReceiverType || !receiverIdSet.Contains(x.ReceiverId)))
|
||||
foreach (var read in existsReads.Where(x => x.ReceiverType != ReceiverTypeUser || !receiverIdSet.Contains(x.ReceiverId)))
|
||||
{
|
||||
await NoticeReadRepository.DeleteAsync(read);
|
||||
}
|
||||
@ -115,8 +147,6 @@ public class NoticeService : ApplicationService
|
||||
/// <summary>
|
||||
/// 删除通知。
|
||||
/// </summary>
|
||||
/// <param name="input">入参:目标主键。</param>
|
||||
/// <returns>无返回体,仅完成删除通知。</returns>
|
||||
[HttpPost("/system/notice/delete")]
|
||||
public async Task DeleteAsync(IdInput input)
|
||||
{
|
||||
@ -127,8 +157,6 @@ public class NoticeService : ApplicationService
|
||||
/// <summary>
|
||||
/// 确认通知已读。
|
||||
/// </summary>
|
||||
/// <param name="input">入参:目标通知主键。</param>
|
||||
/// <returns>无返回体,仅完成当前主体的已读确认。</returns>
|
||||
[HttpPost("/system/notice/read-confirm")]
|
||||
public async Task ReadConfirmAsync(IdInput input)
|
||||
{
|
||||
@ -141,8 +169,7 @@ public class NoticeService : ApplicationService
|
||||
throw new BusinessException("当前登录主体无效");
|
||||
}
|
||||
|
||||
if (!string.Equals(notice.ReceiverType, currentReceiverType, StringComparison.OrdinalIgnoreCase) ||
|
||||
!receiverIds.Contains(currentReceiverId))
|
||||
if (!receiverIds.Contains(currentReceiverId))
|
||||
{
|
||||
throw new BusinessException("当前主体不在通知接收范围内");
|
||||
}
|
||||
@ -162,6 +189,131 @@ public class NoticeService : ApplicationService
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<OmsPageResult<NoticeReceiverOptionOutput>> QueryUserReceiverOptionsAsync(
|
||||
string? keyword,
|
||||
int pageIndex,
|
||||
int pageSize)
|
||||
{
|
||||
var where = $"WHERE u.`IsDeleted` = 0 AND {InternalUserFilter}";
|
||||
var args = new List<object>();
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
var pattern = $"%{keyword}%";
|
||||
where += $" AND (u.`Name` LIKE {{{args.Count}}} OR u.`Account` LIKE {{{args.Count + 1}}})";
|
||||
args.Add(pattern);
|
||||
args.Add(pattern);
|
||||
}
|
||||
|
||||
var countSql = $"SELECT COUNT(*) AS `Value` FROM `user` u {where}";
|
||||
var total = await RbacDbContext.Database.SqlQueryRaw<long>(countSql, args.ToArray()).FirstAsync();
|
||||
var offset = Math.Max(pageIndex - 1, 0) * pageSize;
|
||||
var listSql = $"""
|
||||
SELECT
|
||||
u.`userid` AS `Value`,
|
||||
u.`Name` AS `Label`,
|
||||
u.`Account` AS `Extra`
|
||||
FROM `user` u
|
||||
{where}
|
||||
ORDER BY u.`Name`, u.`userid`
|
||||
LIMIT {pageSize} OFFSET {offset}
|
||||
""";
|
||||
var items = await RbacDbContext.Database.SqlQueryRaw<NoticeReceiverOptionRow>(listSql, args.ToArray())
|
||||
.Select(x => new NoticeReceiverOptionOutput
|
||||
{
|
||||
Value = x.Value,
|
||||
Label = string.IsNullOrWhiteSpace(x.Label) ? $"用户{x.Value}" : x.Label.Trim(),
|
||||
Extra = string.IsNullOrWhiteSpace(x.Extra) ? null : x.Extra.Trim()
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return new OmsPageResult<NoticeReceiverOptionOutput> { Items = items, Total = total };
|
||||
}
|
||||
|
||||
private async Task<OmsPageResult<NoticeReceiverOptionOutput>> QueryCustomerReceiverOptionsAsync(
|
||||
string? keyword,
|
||||
int pageIndex,
|
||||
int pageSize)
|
||||
{
|
||||
Expression<Func<Customer, bool>> predicate = x => x.ParentCustomerId == null;
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
var pattern = keyword.Trim();
|
||||
predicate = x =>
|
||||
x.ParentCustomerId == null &&
|
||||
(x.CustomerName.Contains(pattern) ||
|
||||
x.CustomerCode.Contains(pattern) ||
|
||||
x.Phone.Contains(pattern));
|
||||
}
|
||||
|
||||
var total = await CustomerRepository.CountAsync(predicate);
|
||||
var customers = await CustomerRepository.GetPagedListAsync(
|
||||
predicate,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
new Dictionary<string, bool> { [nameof(Customer.CustomerName)] = true, [nameof(Customer.Id)] = true });
|
||||
|
||||
return new OmsPageResult<NoticeReceiverOptionOutput>
|
||||
{
|
||||
Total = total,
|
||||
Items = customers.Select(x => new NoticeReceiverOptionOutput
|
||||
{
|
||||
Value = x.Id,
|
||||
Label = string.IsNullOrWhiteSpace(x.CustomerName) ? $"客户{x.Id}" : x.CustomerName.Trim(),
|
||||
Extra = string.IsNullOrWhiteSpace(x.CustomerCode) ? x.Phone : x.CustomerCode.Trim()
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Dictionary<int, List<int>>> BuildReceiverSelectionIdMapAsync(IReadOnlyCollection<Notice> notices)
|
||||
{
|
||||
var result = new Dictionary<int, List<int>>();
|
||||
if (notices.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var customerNotices = notices
|
||||
.Where(x => string.Equals(x.ReceiverType, ReceiverTypeCustomer, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
Dictionary<int, List<int>> userIdToCustomerIds = [];
|
||||
if (customerNotices.Count > 0)
|
||||
{
|
||||
var allUserIds = customerNotices
|
||||
.SelectMany(x => OmsDocHelper.DeserializeJson<List<int>>(x.ReceiverIds) ?? [])
|
||||
.Where(x => x > 0)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
if (allUserIds.Count > 0)
|
||||
{
|
||||
var customers = await CustomerRepository.GetListAsync(x =>
|
||||
x.ParentCustomerId == null && allUserIds.Contains(x.UserId));
|
||||
userIdToCustomerIds = customers
|
||||
.GroupBy(x => x.UserId)
|
||||
.ToDictionary(x => x.Key, x => x.Select(c => c.Id).Distinct().ToList());
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var notice in notices)
|
||||
{
|
||||
var storedUserIds = OmsDocHelper.DeserializeJson<List<int>>(notice.ReceiverIds) ?? [];
|
||||
if (string.Equals(notice.ReceiverType, ReceiverTypeCustomer, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var selectionIds = storedUserIds
|
||||
.SelectMany(userId => userIdToCustomerIds.TryGetValue(userId, out var customerIds)
|
||||
? customerIds
|
||||
: [])
|
||||
.Distinct()
|
||||
.ToList();
|
||||
result[notice.Id] = selectionIds.Count > 0 ? selectionIds : storedUserIds;
|
||||
continue;
|
||||
}
|
||||
|
||||
result[notice.Id] = storedUserIds.Distinct().ToList();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Expression<Func<Notice, bool>> BuildListPredicate(NoticeListInput input)
|
||||
{
|
||||
var noticeType = input.NoticeType?.Trim();
|
||||
@ -189,23 +341,47 @@ public class NoticeService : ApplicationService
|
||||
private async Task<List<int>> NormalizeAndValidateReceiverIdsAsync(string receiverType, IEnumerable<int>? receiverIds)
|
||||
{
|
||||
var normalizedReceiverType = NormalizeReceiverType(receiverType);
|
||||
var normalizedIds = (receiverIds ?? [])
|
||||
var selectionIds = (receiverIds ?? [])
|
||||
.Where(x => x > 0)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (normalizedIds.Count == 0)
|
||||
if (selectionIds.Count == 0)
|
||||
{
|
||||
throw new BusinessException("接收人不能为空");
|
||||
}
|
||||
|
||||
var validUserIds = await QueryValidUserIdsAsync(normalizedIds);
|
||||
if (validUserIds.Count != normalizedIds.Count)
|
||||
if (normalizedReceiverType == ReceiverTypeCustomer)
|
||||
{
|
||||
var customers = await CustomerRepository.GetListAsync(x =>
|
||||
selectionIds.Contains(x.Id) && x.ParentCustomerId == null);
|
||||
if (customers.Count != selectionIds.Count)
|
||||
{
|
||||
throw new BusinessException("接收客户包含无效数据");
|
||||
}
|
||||
|
||||
var userIds = customers.Select(x => x.UserId).Where(x => x > 0).Distinct().ToList();
|
||||
if (userIds.Count == 0)
|
||||
{
|
||||
throw new BusinessException("接收客户未关联有效用户");
|
||||
}
|
||||
|
||||
var validUserIds = await QueryValidUserIdsAsync(userIds);
|
||||
if (validUserIds.Count != userIds.Count)
|
||||
{
|
||||
throw new BusinessException("接收客户关联用户无效");
|
||||
}
|
||||
|
||||
return userIds;
|
||||
}
|
||||
|
||||
var validIds = await QueryValidUserIdsAsync(selectionIds);
|
||||
if (validIds.Count != selectionIds.Count)
|
||||
{
|
||||
throw new BusinessException("接收用户包含无效数据");
|
||||
}
|
||||
|
||||
return normalizedIds;
|
||||
return selectionIds;
|
||||
}
|
||||
|
||||
private async Task<List<int>> QueryValidUserIdsAsync(IReadOnlyCollection<int> userIds)
|
||||
@ -234,7 +410,7 @@ public class NoticeService : ApplicationService
|
||||
}
|
||||
}
|
||||
|
||||
private static NoticeOutput MapNoticeOutput(Notice entity, int readCount)
|
||||
private static NoticeOutput MapNoticeOutput(Notice entity, int readCount, List<int> receiverSelectionIds)
|
||||
{
|
||||
var receiverIds = OmsDocHelper.DeserializeJson<List<int>>(entity.ReceiverIds) ?? [];
|
||||
return new NoticeOutput
|
||||
@ -246,8 +422,9 @@ public class NoticeService : ApplicationService
|
||||
Content = entity.Content,
|
||||
IsPopup = entity.IsPopup,
|
||||
ReceiverType = entity.ReceiverType,
|
||||
ReceiverTypeName = "用户",
|
||||
ReceiverTypeName = entity.ReceiverType == ReceiverTypeCustomer ? "客户" : "用户",
|
||||
ReceiverIds = receiverIds,
|
||||
ReceiverSelectionIds = receiverSelectionIds,
|
||||
ReadCount = readCount,
|
||||
ReceiverCount = receiverIds.Distinct().Count(),
|
||||
CreateTime = entity.CreateTime,
|
||||
@ -272,6 +449,7 @@ public class NoticeService : ApplicationService
|
||||
return normalized switch
|
||||
{
|
||||
ReceiverTypeUser => ReceiverTypeUser,
|
||||
ReceiverTypeCustomer => ReceiverTypeCustomer,
|
||||
_ => throw new BusinessException("接收对象类型无效")
|
||||
};
|
||||
}
|
||||
@ -290,4 +468,13 @@ public class NoticeService : ApplicationService
|
||||
{
|
||||
public int UserId { get; set; }
|
||||
}
|
||||
|
||||
private sealed class NoticeReceiverOptionRow
|
||||
{
|
||||
public int Value { get; set; }
|
||||
|
||||
public string? Label { get; set; }
|
||||
|
||||
public string? Extra { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,12 @@ public class OmsWxLoginInput
|
||||
public string LoginUuid { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class OmsCustomerOmsLoginInput
|
||||
{
|
||||
[Required]
|
||||
public int CustomerId { get; set; }
|
||||
}
|
||||
|
||||
public class OmsChangePasswordInput
|
||||
{
|
||||
[Required]
|
||||
|
||||
@ -62,6 +62,53 @@ public partial class AuthService(
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 管理员代登客户门户(免密)。
|
||||
/// </summary>
|
||||
/// <param name="input">入参:目标客户主键。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>客户门户访问令牌。</returns>
|
||||
[HttpPost("/rbac/auth/customer-oms-login")]
|
||||
public async Task<LoginResultDto> CustomerOmsLoginAsync(
|
||||
[FromBody] OmsCustomerOmsLoginInput input,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureAdminPrincipal();
|
||||
|
||||
var customer = await customerRepository.FirstOrDefaultAsync(x => x.Id == input.CustomerId, cancellationToken);
|
||||
if (customer == null)
|
||||
{
|
||||
throw new BusinessException("客户不存在");
|
||||
}
|
||||
|
||||
if (customer.ParentCustomerId.HasValue)
|
||||
{
|
||||
ValidateSubCustomerStatus(customer);
|
||||
var mainCustomer = await customerRepository.FirstOrDefaultAsync(
|
||||
x => x.Id == customer.ParentCustomerId.Value,
|
||||
cancellationToken);
|
||||
if (mainCustomer == null)
|
||||
{
|
||||
throw new BusinessException("主客户不存在");
|
||||
}
|
||||
|
||||
ValidateMainCustomerStatus(mainCustomer);
|
||||
}
|
||||
else
|
||||
{
|
||||
ValidateMainCustomerStatus(customer);
|
||||
}
|
||||
|
||||
var userGuid = await GetRbacUserGuidByMapIdAsync(customer.UserId, cancellationToken);
|
||||
if (!userGuid.HasValue)
|
||||
{
|
||||
throw new BusinessException("客户未关联用户");
|
||||
}
|
||||
|
||||
var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
|
||||
return BuildLoginResult(userGuid.Value, claims);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 微信扫码登录轮询状态(key 是否已有绑定账号)。
|
||||
/// </summary>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user