| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- using Admin.NET.Core;
- using Admin.NET.Plugin.AiDOP.Const.S8;
- using Admin.NET.Plugin.AiDOP.Entity.S8;
- using Admin.NET.Plugin.AiDOP.Infrastructure;
- using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
- using Microsoft.Extensions.Logging;
- namespace Admin.NET.Plugin.AiDOP.Service.S8;
- /// <summary>配置页的一行:一个事件 + 它当前配置的收件人类型。</summary>
- public sealed class S8NotifyEventRowDto
- {
- public string EventCode { get; set; } = string.Empty;
- public string DisplayName { get; set; } = string.Empty;
- public string TriggerHint { get; set; } = string.Empty;
- public int OrderNo { get; set; }
- public List<string> RecipientTypes { get; set; } = new();
- /// <summary>SPECIFIC_USER 选中的账号。</summary>
- public List<long> SpecificUserIds { get; set; } = new();
- /// <summary>本事件尚未配置任何收件人 —— 会回落 LEGACY 分层,页面必须说明。</summary>
- public bool NotConfigured => RecipientTypes.Count == 0;
- }
- /// <summary>
- /// 「通知设置」页的一行:一个事件 + 它提醒谁(业务文案)+ 是否启用。
- /// <para>页面上不出现任何收件人技术枚举 —— 对象由责任关系推导。</para>
- /// </summary>
- public sealed class S8NotifyEventSwitchDto
- {
- public string EventCode { get; set; } = string.Empty;
- public string DisplayName { get; set; } = string.Empty;
- public string TriggerHint { get; set; } = string.Empty;
- public int OrderNo { get; set; }
- /// <summary>是否已在新模型里启用通知。</summary>
- public bool Enabled { get; set; }
- /// <summary>提醒谁(业务文案,如「处理人员」「当前复核人」)。只读展示。</summary>
- public string RecipientLabel { get; set; } = string.Empty;
- /// <summary>当前配置是否就是代码级默认推导。false = 历史手工配置与默认不同,页面需提示。</summary>
- public bool MatchesDerivedDefault { get; set; } = true;
- }
- /// <summary>
- /// S8-NOTIFY-RECIPIENT-1:收件人配置的读写。
- ///
- /// <para><b>写入侧只认目录内的事件与类型</b>,并对 SPECIFIC_USER 逐个校验账号
- /// (同租户 + 启用,复用 <see cref="IS8UserScopeValidator"/>)——
- /// 让「配得进去、发的时候又发不出」这种状态在写入那一刻就不可能产生。</para>
- /// </summary>
- public class S8NotificationRecipientConfigService : ITransient
- {
- private readonly SqlSugarRepository<AdoS8NotificationRecipient> _rep;
- private readonly IS8UserScopeValidator _userScope;
- private readonly UserManager _userManager;
- private readonly ILogger<S8NotificationRecipientConfigService> _logger;
- public S8NotificationRecipientConfigService(
- SqlSugarRepository<AdoS8NotificationRecipient> rep,
- IS8UserScopeValidator userScope,
- UserManager userManager,
- ILogger<S8NotificationRecipientConfigService> logger)
- {
- _rep = rep;
- _userScope = userScope;
- _userManager = userManager;
- _logger = logger;
- }
- /// <summary>某条规则(或租户默认)的事件矩阵。目录里每个事件都出一行,未配置的也要出现。</summary>
- public async Task<List<S8NotifyEventRowDto>> GetMatrixAsync(long tenantId, string ruleCode)
- {
- var rows = await _rep.AsQueryable().ClearFilter()
- .Where(x => x.TenantId == tenantId && x.RuleCode == ruleCode)
- .ToListAsync();
- var byEvent = rows.GroupBy(x => x.EventCode, StringComparer.OrdinalIgnoreCase)
- .ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
- return S8NotificationCatalog.Events.OrderBy(e => e.OrderNo).Select(e =>
- {
- var cfg = byEvent.GetValueOrDefault(e.Code) ?? new List<AdoS8NotificationRecipient>();
- var specific = cfg.FirstOrDefault(c => c.RecipientType == S8RecipientType.SpecificUser);
- return new S8NotifyEventRowDto
- {
- EventCode = e.Code,
- DisplayName = e.DisplayName,
- TriggerHint = e.TriggerHint,
- OrderNo = e.OrderNo,
- RecipientTypes = cfg.Select(c => c.RecipientType).Distinct().ToList(),
- SpecificUserIds = S8RoleResolver.SplitTokens(specific?.RecipientUserIds)
- .Select(t => long.TryParse(t, out var v) ? v : 0).Where(v => v > 0).ToList()
- };
- }).ToList();
- }
- /// <summary>
- /// S8-RESPONSIBILITY-POOL-1:业务视角的事件开关矩阵。
- ///
- /// <para>业务用户只回答「这件事要不要提醒」;<b>提醒谁由责任关系推导</b>
- /// (见 <see cref="S8NotificationCatalog.DefaultRecipientByEvent"/>),
- /// 页面上不出现 <c>HANDLER_POOL</c> / <c>ASSIGNEE</c> 这类技术枚举。</para>
- /// </summary>
- public async Task<List<S8NotifyEventSwitchDto>> GetEventSwitchesAsync(long tenantId, string ruleCode)
- {
- var matrix = await GetMatrixAsync(tenantId, ruleCode);
- var byEvent = matrix.ToDictionary(m => m.EventCode, StringComparer.OrdinalIgnoreCase);
- return S8NotificationCatalog.Events.OrderBy(e => e.OrderNo).Select(e =>
- {
- var derived = S8NotificationCatalog.ResolveDefaultRecipientType(e.Code);
- var cfg = byEvent.GetValueOrDefault(e.Code);
- var configured = cfg?.RecipientTypes ?? new List<string>();
- // 「已启用」= 该事件在新模型里有任何收件人配置。
- // 未启用时刻意**不**声称"不会通知"——旧 LEGACY 分层仍可能兜底,
- // 页面据此显示"未启用(可能仍走旧配置)",而不是给一个会骗人的关闭态。
- return new S8NotifyEventSwitchDto
- {
- EventCode = e.Code,
- DisplayName = e.DisplayName,
- TriggerHint = e.TriggerHint,
- OrderNo = e.OrderNo,
- Enabled = configured.Count > 0,
- RecipientLabel = S8NotificationCatalog.DescribeRecipientType(
- configured.FirstOrDefault() ?? derived),
- // 已配置的类型与代码级默认不一致 —— 通常是上一版手工配过。
- // 不静默改写,只标出来让管理员知道这一行与默认推导不同。
- MatchesDerivedDefault = configured.Count == 0
- || (configured.Count == 1 && string.Equals(
- configured[0], derived, StringComparison.OrdinalIgnoreCase)),
- };
- }).ToList();
- }
- /// <summary>
- /// 按事件开关写入 / 移除收件人配置。收件人类型由目录推导,<b>调用方不传</b>。
- ///
- /// <para>这是「通知设置」页唯一的写入口:业务用户点的是开关,
- /// 落库的仍是既有的收件人模型 —— 内部实现保留,只是不再暴露给页面。</para>
- /// </summary>
- public async Task SetEventEnabledAsync(S8TrustedScope scope, string ruleCode, string eventCode, bool enabled)
- {
- if (!S8NotificationCatalog.IsKnownEvent(eventCode))
- throw new S8BizException($"未知事件码:{eventCode}");
- var derived = S8NotificationCatalog.ResolveDefaultRecipientType(eventCode)
- ?? throw new S8BizException($"事件 {eventCode} 没有默认收件人推导规则,无法按开关配置");
- await SetAsync(scope, ruleCode, eventCode,
- enabled ? new[] { derived } : Array.Empty<string>(), null);
- }
- /// <summary>全量替换某事件的收件人配置。</summary>
- public async Task SetAsync(S8TrustedScope scope, string ruleCode, string eventCode,
- IEnumerable<string>? recipientTypes, IEnumerable<long>? specificUserIds)
- {
- if (string.IsNullOrWhiteSpace(ruleCode)) throw new S8BizException("规则编码不能为空");
- if (!S8NotificationCatalog.IsKnownEvent(eventCode))
- throw new S8BizException($"未知事件码:{eventCode}");
- var types = (recipientTypes ?? Enumerable.Empty<string>())
- .Where(t => !string.IsNullOrWhiteSpace(t)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
- var unknown = types.Where(t => !S8NotificationCatalog.IsKnownRecipientType(t)).ToList();
- if (unknown.Count > 0)
- throw new S8BizException($"未知收件人类型:{string.Join('、', unknown)}");
- var users = (specificUserIds ?? Enumerable.Empty<long>()).Where(x => x > 0).Distinct().ToList();
- if (types.Contains(S8RecipientType.SpecificUser, StringComparer.OrdinalIgnoreCase))
- {
- if (users.Count == 0)
- throw new S8BizException("已选择「指定账号」,请至少选择一个账号");
- var valid = (await _userScope.ValidateUsersAsync(scope.TenantId, users)).Select(u => u.UserId).ToHashSet();
- if (users.Any(id => !valid.Contains(id)))
- // 与人员校验同口径:跨租户与不存在共用同一文案,不泄漏存在性。
- throw new S8BizException("所选账号不存在或已停用");
- }
- else
- {
- // 没勾「指定账号」就不该留下账号列表 —— 留着会在下次勾选时悄悄复活一批旧人选。
- users.Clear();
- }
- await _rep.AsDeleteable()
- .Where(x => x.TenantId == scope.TenantId && x.RuleCode == ruleCode && x.EventCode == eventCode)
- .ExecuteCommandAsync();
- if (types.Count > 0)
- {
- var now = DateTime.Now;
- await _rep.AsInsertable(types.Select(t => new AdoS8NotificationRecipient
- {
- TenantId = scope.TenantId,
- RuleCode = ruleCode,
- EventCode = eventCode,
- RecipientType = t,
- RecipientUserIds = t.Equals(S8RecipientType.SpecificUser, StringComparison.OrdinalIgnoreCase)
- ? string.Join(',', users)
- : null,
- CreatedAt = now,
- CreatedBy = _userManager.Account
- }).ToList()).ExecuteCommandAsync();
- }
- _logger.LogInformation(
- "s8_notify_recipient_updated tenant={Tenant} rule={Rule} event={Event} types={Types} users={Users}",
- scope.TenantId, ruleCode, eventCode, string.Join(',', types), users.Count);
- }
- }
|