diff --git a/db.sql b/db.sql
index 9f81c85..c37356e 100644
--- a/db.sql
+++ b/db.sql
@@ -25,6 +25,7 @@ DROP TABLE IF EXISTS `t_label_print_record`;
DROP TABLE IF EXISTS `t_outbound_record`;
DROP TABLE IF EXISTS `t_inbound_record`;
DROP TABLE IF EXISTS `t_label_template`;
+DROP TABLE IF EXISTS `t_issue_template`;
DROP TABLE IF EXISTS `t_auto_trigger`;
DROP TABLE IF EXISTS `t_weight_rounding_info`;
DROP TABLE IF EXISTS `t_weight_rounding`;
@@ -653,6 +654,24 @@ CREATE TABLE `t_auto_trigger` (
KEY `idx_at_operation` (`Operation`)
) COMMENT='自动触发表';
+CREATE TABLE `t_issue_template` (
+ `Id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+ `OrganizationId` CHAR(36) NULL COMMENT '归属组织 GUID;NULL=平台通用',
+ `IssueType` VARCHAR(32) NOT NULL COMMENT '问题类型 code',
+ `Title` VARCHAR(128) NOT NULL COMMENT '问题标题',
+ `Content` TEXT NOT NULL COMMENT '问题内容',
+ `NotifyMethod` VARCHAR(64) NOT NULL COMMENT '通知方式,逗号分隔',
+ `Status` TINYINT NOT NULL DEFAULT 1 COMMENT '0-禁用 1-启用',
+ `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
+ `CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
+ `CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `UpdateUserId` INT NULL COMMENT '更新人',
+ `UpdateTime` DATETIME NULL COMMENT '更新时间',
+ PRIMARY KEY (`Id`),
+ KEY `idx_issue_tpl_org_type` (`OrganizationId`, `IssueType`, `IsDeleted`),
+ KEY `idx_issue_tpl_status` (`Status`, `IsDeleted`)
+) COMMENT='渠道-问题模板';
+
CREATE TABLE `t_label_template` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1',
`Name` VARCHAR(128) NOT NULL COMMENT '模板名称',
diff --git a/sql/migrate_t_issue_template.sql b/sql/migrate_t_issue_template.sql
new file mode 100644
index 0000000..f88142d
--- /dev/null
+++ b/sql/migrate_t_issue_template.sql
@@ -0,0 +1,18 @@
+-- 渠道-问题模板
+CREATE TABLE IF NOT EXISTS `t_issue_template` (
+ `Id` BIGINT NOT NULL AUTO_INCREMENT,
+ `OrganizationId` CHAR(36) NULL COMMENT '归属组织 GUID;NULL=平台通用',
+ `IssueType` VARCHAR(32) NOT NULL COMMENT '问题类型 code',
+ `Title` VARCHAR(128) NOT NULL COMMENT '问题标题',
+ `Content` TEXT NOT NULL COMMENT '问题内容',
+ `NotifyMethod` VARCHAR(64) NOT NULL COMMENT '通知方式,逗号分隔',
+ `Status` TINYINT NOT NULL DEFAULT 1 COMMENT '0-禁用 1-启用',
+ `IsDeleted` TINYINT NOT NULL DEFAULT 0,
+ `CreateUserId` INT NOT NULL,
+ `CreateTime` DATETIME NOT NULL,
+ `UpdateUserId` INT NULL,
+ `UpdateTime` DATETIME NULL,
+ PRIMARY KEY (`Id`),
+ KEY `idx_issue_tpl_org_type` (`OrganizationId`, `IssueType`, `IsDeleted`),
+ KEY `idx_issue_tpl_status` (`Status`, `IsDeleted`)
+) COMMENT='渠道-问题模板';
diff --git a/src/Oms.Application/Dtos/Channels/IssueTemplateListInput.cs b/src/Oms.Application/Dtos/Channels/IssueTemplateListInput.cs
new file mode 100644
index 0000000..3276368
--- /dev/null
+++ b/src/Oms.Application/Dtos/Channels/IssueTemplateListInput.cs
@@ -0,0 +1,19 @@
+namespace Oms.Application.Dtos.Channels;
+
+///
+/// 问题模板列表查询入参。
+///
+public class IssueTemplateListInput
+{
+ public int Page { get; set; } = 1;
+
+ public int PageSize { get; set; } = 20;
+
+ public string? OrganizationId { get; set; }
+
+ public string? IssueType { get; set; }
+
+ public string? Keyword { get; set; }
+
+ public byte? Status { get; set; }
+}
diff --git a/src/Oms.Application/Dtos/Channels/IssueTemplateOptionOutput.cs b/src/Oms.Application/Dtos/Channels/IssueTemplateOptionOutput.cs
new file mode 100644
index 0000000..32030cf
--- /dev/null
+++ b/src/Oms.Application/Dtos/Channels/IssueTemplateOptionOutput.cs
@@ -0,0 +1,15 @@
+namespace Oms.Application.Dtos.Channels;
+
+///
+/// 问题模板下拉选项。
+///
+public class IssueTemplateOptionOutput
+{
+ public int Id { get; set; }
+
+ public string IssueType { get; set; } = string.Empty;
+
+ public string? IssueTypeName { get; set; }
+
+ public string Title { get; set; } = string.Empty;
+}
diff --git a/src/Oms.Application/Dtos/Channels/IssueTemplateOutput.cs b/src/Oms.Application/Dtos/Channels/IssueTemplateOutput.cs
new file mode 100644
index 0000000..aa30bb5
--- /dev/null
+++ b/src/Oms.Application/Dtos/Channels/IssueTemplateOutput.cs
@@ -0,0 +1,31 @@
+namespace Oms.Application.Dtos.Channels;
+
+///
+/// 问题模板出参。
+///
+public class IssueTemplateOutput
+{
+ public int Id { get; set; }
+
+ public string? OrganizationId { get; set; }
+
+ public string? OrganizationName { get; set; }
+
+ public string IssueType { get; set; } = string.Empty;
+
+ public string? IssueTypeName { get; set; }
+
+ public string Title { get; set; } = string.Empty;
+
+ public string Content { get; set; } = string.Empty;
+
+ public string NotifyMethod { get; set; } = string.Empty;
+
+ public List NotifyMethods { get; set; } = [];
+
+ public byte Status { get; set; }
+
+ public DateTime CreateTime { get; set; }
+
+ public DateTime? UpdateTime { get; set; }
+}
diff --git a/src/Oms.Application/Dtos/Channels/IssueTemplateSaveInput.cs b/src/Oms.Application/Dtos/Channels/IssueTemplateSaveInput.cs
new file mode 100644
index 0000000..9ae17c6
--- /dev/null
+++ b/src/Oms.Application/Dtos/Channels/IssueTemplateSaveInput.cs
@@ -0,0 +1,23 @@
+using Oms.Domain.Shared.Enums;
+
+namespace Oms.Application.Dtos.Channels;
+
+///
+/// 问题模板保存入参。
+///
+public class IssueTemplateSaveInput
+{
+ public int? Id { get; set; }
+
+ public string? OrganizationId { get; set; }
+
+ public string? IssueType { get; set; }
+
+ public string? Title { get; set; }
+
+ public string? Content { get; set; }
+
+ public string? NotifyMethod { get; set; }
+
+ public EnableStatus Status { get; set; } = EnableStatus.Enabled;
+}
diff --git a/src/Oms.Application/Services/Channels/IssueTemplateService.cs b/src/Oms.Application/Services/Channels/IssueTemplateService.cs
new file mode 100644
index 0000000..4cb2ed6
--- /dev/null
+++ b/src/Oms.Application/Services/Channels/IssueTemplateService.cs
@@ -0,0 +1,303 @@
+using System.Linq.Expressions;
+using Fake.Application;
+using Microsoft.AspNetCore.Mvc;
+using Oms.Application.DataPerm;
+using Oms.Application.Dtos.Channels;
+using Oms.Application.Dtos.Shared;
+using Oms.Application.Shared;
+using Oms.Domain.ChannelsAggregate.Consts;
+using Oms.Domain.ChannelsAggregate.Entities;
+using Oms.Domain.ChannelsAggregate.Repositories;
+using Oms.Domain.Shared.Enums;
+
+namespace Oms.Application.Services.Channels;
+
+///
+/// 问题模板 API 服务。
+///
+[ApiExplorerSettings(GroupName = "OMS-Channel")]
+public class IssueTemplateService : ApplicationService
+{
+ private IIssueTemplateRepository IssueTemplateRepository =>
+ LazyServiceProvider.GetRequiredService();
+
+ private DataPermRepository DataPermRepository =>
+ LazyServiceProvider.GetRequiredService();
+
+ [HttpGet("/channel/issue-template/list")]
+ public async Task> GetListAsync([FromQuery] IssueTemplateListInput input)
+ {
+ var predicate = BuildListPredicate(input);
+ var pageIndex = OmsDocHelper.NormalizePage(input.Page);
+ var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
+ var entities = await IssueTemplateRepository.GetPagedListAsync(
+ predicate,
+ pageIndex,
+ pageSize,
+ new Dictionary { [nameof(IssueTemplate.UpdateTime)] = false, [nameof(IssueTemplate.Id)] = false });
+ var total = await IssueTemplateRepository.CountAsync(predicate);
+ var orgNameMap = await ResolveOrganizationNameMapAsync(entities);
+ var items = entities.Select(x => MapOutput(x, orgNameMap)).ToList();
+ return new OmsPageResult { Items = items, Total = total };
+ }
+
+ [HttpGet("/channel/issue-template/detail")]
+ public async Task GetDetailAsync([FromQuery] int id)
+ {
+ var entity = await GetEntityOrThrowAsync(id);
+ var orgNameMap = await ResolveOrganizationNameMapAsync([entity]);
+ return MapOutput(entity, orgNameMap);
+ }
+
+ [HttpPost("/channel/issue-template/create")]
+ public async Task CreateAsync(IssueTemplateSaveInput input)
+ {
+ var normalized = await NormalizeSaveInputAsync(input);
+ var entity = new IssueTemplate
+ {
+ OrganizationId = normalized.OrganizationId,
+ IssueType = normalized.IssueType,
+ Title = normalized.Title,
+ Content = normalized.Content,
+ NotifyMethod = normalized.NotifyMethod,
+ Status = normalized.Status
+ };
+ await IssueTemplateRepository.InsertAsync(entity);
+ }
+
+ [HttpPost("/channel/issue-template/update")]
+ public async Task UpdateAsync(IssueTemplateSaveInput input)
+ {
+ var entity = await GetEntityOrThrowAsync(input.Id);
+ var normalized = await NormalizeSaveInputAsync(input, input.Id);
+ entity.OrganizationId = normalized.OrganizationId;
+ entity.IssueType = normalized.IssueType;
+ entity.Title = normalized.Title;
+ entity.Content = normalized.Content;
+ entity.NotifyMethod = normalized.NotifyMethod;
+ entity.Status = normalized.Status;
+ await IssueTemplateRepository.UpdateAsync(entity);
+ }
+
+ [HttpPost("/channel/issue-template/delete")]
+ public async Task DeleteAsync(IdInput input)
+ {
+ var entity = await GetEntityOrThrowAsync(input.Id);
+ await IssueTemplateRepository.DeleteAsync(entity);
+ }
+
+ [HttpPost("/channel/issue-template/toggleStatus")]
+ public async Task ToggleStatusAsync(ToggleStatusInput input)
+ {
+ var entity = await GetEntityOrThrowAsync(input.Id);
+ entity.Status = input.Status;
+ await IssueTemplateRepository.UpdateAsync(entity);
+ }
+
+ [HttpGet("/channel/issue-template/options")]
+ public async Task> GetOptionsAsync(
+ [FromQuery] string? organizationId = null,
+ [FromQuery] string? issueType = null)
+ {
+ var normalizedOrg = NormalizeOrganizationId(organizationId);
+ var normalizedType = issueType?.Trim();
+
+ var entities = await IssueTemplateRepository.GetListAsync(x =>
+ x.Status == EnableStatus.Enabled
+ && (string.IsNullOrWhiteSpace(normalizedType) || x.IssueType == normalizedType)
+ && (normalizedOrg == null
+ || x.OrganizationId == normalizedOrg
+ || x.OrganizationId == null
+ || x.OrganizationId == string.Empty));
+
+ var grouped = entities
+ .GroupBy(x => x.IssueType, StringComparer.OrdinalIgnoreCase)
+ .Select(group =>
+ {
+ var preferred = group.FirstOrDefault(x => x.OrganizationId == normalizedOrg)
+ ?? group.FirstOrDefault(x => string.IsNullOrWhiteSpace(x.OrganizationId))
+ ?? group.First();
+ return new IssueTemplateOptionOutput
+ {
+ Id = preferred.Id,
+ IssueType = preferred.IssueType,
+ IssueTypeName = IssueTypeCodes.GetDisplayName(preferred.IssueType),
+ Title = preferred.Title
+ };
+ })
+ .OrderBy(x => x.IssueType, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ return new OmsItemsResult { Items = grouped };
+ }
+
+ private static Expression> BuildListPredicate(IssueTemplateListInput input)
+ {
+ var organizationId = NormalizeOrganizationId(input.OrganizationId);
+ var issueType = input.IssueType?.Trim();
+ var keyword = input.Keyword?.Trim();
+ var status = input.Status;
+
+ return entity =>
+ (organizationId == null || entity.OrganizationId == organizationId)
+ && (string.IsNullOrWhiteSpace(issueType) || entity.IssueType == issueType)
+ && (status == null || (byte)entity.Status == status.Value)
+ && (string.IsNullOrWhiteSpace(keyword)
+ || entity.Title.Contains(keyword)
+ || entity.Content.Contains(keyword));
+ }
+
+ private async Task NormalizeSaveInputAsync(
+ IssueTemplateSaveInput input,
+ int? excludeId = null)
+ {
+ var issueType = input.IssueType?.Trim().ToLowerInvariant();
+ if (string.IsNullOrWhiteSpace(issueType) || !IssueTypeCodes.All.Contains(issueType))
+ {
+ throw new BusinessException("问题类型不合法");
+ }
+
+ var title = input.Title?.Trim();
+ if (string.IsNullOrWhiteSpace(title))
+ {
+ throw new BusinessException("问题标题不能为空");
+ }
+
+ if (title.Length > 128)
+ {
+ throw new BusinessException("问题标题长度不能超过128");
+ }
+
+ var content = input.Content?.Trim();
+ if (string.IsNullOrWhiteSpace(content))
+ {
+ throw new BusinessException("问题内容不能为空");
+ }
+
+ if (content.Length > 5000)
+ {
+ throw new BusinessException("问题内容长度不能超过5000");
+ }
+
+ var notifyMethod = IssueNotifyMethodCodes.Normalize(input.NotifyMethod);
+ if (string.IsNullOrWhiteSpace(notifyMethod))
+ {
+ throw new BusinessException("请至少选择一种通知方式");
+ }
+
+ var organizationId = NormalizeOrganizationId(input.OrganizationId);
+ if (!string.IsNullOrWhiteSpace(organizationId))
+ {
+ if (!Guid.TryParse(organizationId, out var orgGuid))
+ {
+ throw new BusinessException("组织ID格式不正确");
+ }
+
+ if (!await DataPermRepository.OrganizationExistsAndEnabledAsync(orgGuid))
+ {
+ throw new BusinessException("组织不存在");
+ }
+ }
+
+ if (await IssueTemplateRepository.ExistsDuplicateAsync(organizationId, issueType, excludeId))
+ {
+ throw new BusinessException("同一组织下该问题类型已存在模板");
+ }
+
+ return new NormalizedIssueTemplateSaveInput
+ {
+ OrganizationId = organizationId,
+ IssueType = issueType,
+ Title = title,
+ Content = content,
+ NotifyMethod = notifyMethod,
+ Status = input.Status
+ };
+ }
+
+ private static string? NormalizeOrganizationId(string? organizationId)
+ {
+ var normalized = organizationId?.Trim();
+ return string.IsNullOrWhiteSpace(normalized) ? null : normalized;
+ }
+
+ private async Task GetEntityOrThrowAsync(int? id)
+ {
+ if (!id.HasValue || id.Value <= 0)
+ {
+ throw new BusinessException("问题模板不存在");
+ }
+
+ var entity = await IssueTemplateRepository.FirstOrDefaultAsync(x => x.Id == id.Value);
+ if (entity == null)
+ {
+ throw new BusinessException("问题模板不存在");
+ }
+
+ return entity;
+ }
+
+ private async Task> ResolveOrganizationNameMapAsync(
+ IReadOnlyCollection entities)
+ {
+ var orgIds = entities
+ .Select(x => x.OrganizationId)
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(x => Guid.TryParse(x, out var guid) ? guid : Guid.Empty)
+ .Where(x => x != Guid.Empty)
+ .Distinct()
+ .ToList();
+
+ return orgIds.Count == 0
+ ? []
+ : await DataPermRepository.GetOrganizationNamesAsync(orgIds);
+ }
+
+ private static IssueTemplateOutput MapOutput(
+ IssueTemplate entity,
+ IReadOnlyDictionary orgNameMap)
+ {
+ string? organizationName = null;
+ if (!string.IsNullOrWhiteSpace(entity.OrganizationId)
+ && Guid.TryParse(entity.OrganizationId, out var orgGuid)
+ && orgNameMap.TryGetValue(orgGuid, out var name))
+ {
+ organizationName = name;
+ }
+ else if (string.IsNullOrWhiteSpace(entity.OrganizationId))
+ {
+ organizationName = "平台通用";
+ }
+
+ return new IssueTemplateOutput
+ {
+ Id = entity.Id,
+ OrganizationId = entity.OrganizationId,
+ OrganizationName = organizationName,
+ IssueType = entity.IssueType,
+ IssueTypeName = IssueTypeCodes.GetDisplayName(entity.IssueType),
+ Title = entity.Title,
+ Content = entity.Content,
+ NotifyMethod = entity.NotifyMethod,
+ NotifyMethods = IssueNotifyMethodCodes.ParseList(entity.NotifyMethod),
+ Status = (byte)entity.Status,
+ CreateTime = entity.CreateTime,
+ UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
+ };
+ }
+
+ private sealed class NormalizedIssueTemplateSaveInput
+ {
+ public string? OrganizationId { get; set; }
+
+ public string IssueType { get; set; } = string.Empty;
+
+ public string Title { get; set; } = string.Empty;
+
+ public string Content { get; set; } = string.Empty;
+
+ public string NotifyMethod { get; set; } = string.Empty;
+
+ public EnableStatus Status { get; set; }
+ }
+}
diff --git a/src/Oms.Domain/ChannelsAggregate/Consts/IssueNotifyMethodCodes.cs b/src/Oms.Domain/ChannelsAggregate/Consts/IssueNotifyMethodCodes.cs
new file mode 100644
index 0000000..3d495ec
--- /dev/null
+++ b/src/Oms.Domain/ChannelsAggregate/Consts/IssueNotifyMethodCodes.cs
@@ -0,0 +1,57 @@
+namespace Oms.Domain.ChannelsAggregate.Consts;
+
+public static class IssueNotifyMethodCodes
+{
+ public const string InApp = "inApp";
+ public const string Email = "email";
+ public const string Sms = "sms";
+ public const string Feishu = "feishu";
+ public const string Wechat = "wechat";
+
+ public static readonly HashSet All =
+ [
+ InApp,
+ Email,
+ Sms,
+ Feishu,
+ Wechat
+ ];
+
+ public static string GetDisplayName(string? code) => code?.Trim().ToLowerInvariant() switch
+ {
+ InApp => "站内信",
+ Email => "邮件",
+ Sms => "短信",
+ Feishu => "飞书",
+ Wechat => "微信",
+ _ => code ?? string.Empty
+ };
+
+ public static string Normalize(string? raw)
+ {
+ if (string.IsNullOrWhiteSpace(raw))
+ {
+ return string.Empty;
+ }
+
+ var parts = raw
+ .Split([',', ';', ' '], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Select(x => x.ToLowerInvariant())
+ .Where(x => All.Contains(x))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ return string.Join(",", parts);
+ }
+
+ public static List ParseList(string? raw)
+ {
+ var normalized = Normalize(raw);
+ if (string.IsNullOrWhiteSpace(normalized))
+ {
+ return [];
+ }
+
+ return normalized.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
+ }
+}
diff --git a/src/Oms.Domain/ChannelsAggregate/Consts/IssueTypeCodes.cs b/src/Oms.Domain/ChannelsAggregate/Consts/IssueTypeCodes.cs
new file mode 100644
index 0000000..baf5d19
--- /dev/null
+++ b/src/Oms.Domain/ChannelsAggregate/Consts/IssueTypeCodes.cs
@@ -0,0 +1,38 @@
+namespace Oms.Domain.ChannelsAggregate.Consts;
+
+public static class IssueTypeCodes
+{
+ public const string Address = "address";
+ public const string Customs = "customs";
+ public const string Weight = "weight";
+ public const string Document = "document";
+ public const string Cargo = "cargo";
+ public const string Channel = "channel";
+ public const string Payment = "payment";
+ public const string Other = "other";
+
+ public static readonly HashSet All =
+ [
+ Address,
+ Customs,
+ Weight,
+ Document,
+ Cargo,
+ Channel,
+ Payment,
+ Other
+ ];
+
+ public static string GetDisplayName(string? code) => code?.Trim().ToLowerInvariant() switch
+ {
+ Address => "地址异常",
+ Customs => "清关问题",
+ Weight => "重量异常",
+ Document => "单证不全",
+ Cargo => "货物异常",
+ Channel => "渠道异常",
+ Payment => "费用异常",
+ Other => "其他",
+ _ => code ?? string.Empty
+ };
+}
diff --git a/src/Oms.Domain/ChannelsAggregate/Entities/IssueTemplate.cs b/src/Oms.Domain/ChannelsAggregate/Entities/IssueTemplate.cs
new file mode 100644
index 0000000..4a07876
--- /dev/null
+++ b/src/Oms.Domain/ChannelsAggregate/Entities/IssueTemplate.cs
@@ -0,0 +1,21 @@
+using Oms.Domain.Shared;
+using Oms.Domain.Shared.Enums;
+using SqlSugar;
+
+namespace Oms.Domain.ChannelsAggregate.Entities;
+
+[SugarTable("t_issue_template")]
+public class IssueTemplate : OmsAuditedAggregateRoot
+{
+ public string? OrganizationId { get; set; }
+
+ public string IssueType { get; set; } = string.Empty;
+
+ public string Title { get; set; } = string.Empty;
+
+ public string Content { get; set; } = string.Empty;
+
+ public string NotifyMethod { get; set; } = string.Empty;
+
+ public EnableStatus Status { get; set; } = EnableStatus.Enabled;
+}
diff --git a/src/Oms.Domain/ChannelsAggregate/Repositories/IIssueTemplateRepository.cs b/src/Oms.Domain/ChannelsAggregate/Repositories/IIssueTemplateRepository.cs
new file mode 100644
index 0000000..dd0f7aa
--- /dev/null
+++ b/src/Oms.Domain/ChannelsAggregate/Repositories/IIssueTemplateRepository.cs
@@ -0,0 +1,13 @@
+using Fake.Domain.Repositories;
+using Oms.Domain.ChannelsAggregate.Entities;
+
+namespace Oms.Domain.ChannelsAggregate.Repositories;
+
+public interface IIssueTemplateRepository : IRepository
+{
+ Task ExistsDuplicateAsync(
+ string? organizationId,
+ string issueType,
+ int? excludeId = null,
+ CancellationToken cancellationToken = default);
+}
diff --git a/src/Oms.SqlSugarCore/Channels/IssueTemplateRepository.cs b/src/Oms.SqlSugarCore/Channels/IssueTemplateRepository.cs
new file mode 100644
index 0000000..2dfdd46
--- /dev/null
+++ b/src/Oms.SqlSugarCore/Channels/IssueTemplateRepository.cs
@@ -0,0 +1,27 @@
+using Fake.DependencyInjection;
+using Oms.Domain.ChannelsAggregate.Entities;
+using Oms.Domain.ChannelsAggregate.Repositories;
+using Oms.SqlSugarCore.Repositories;
+
+namespace Oms.SqlSugarCore.Channels;
+
+public class IssueTemplateRepository : OmsSqlSugarRepository, IIssueTemplateRepository, IScopedDependency
+{
+ public async Task ExistsDuplicateAsync(
+ string? organizationId,
+ string issueType,
+ int? excludeId = null,
+ CancellationToken cancellationToken = default)
+ {
+ var normalizedOrg = string.IsNullOrWhiteSpace(organizationId) ? null : organizationId.Trim();
+ var normalizedType = issueType.Trim();
+
+ return await AnyAsync(
+ x =>
+ x.IssueType == normalizedType
+ && ((normalizedOrg == null && (x.OrganizationId == null || x.OrganizationId == string.Empty))
+ || x.OrganizationId == normalizedOrg)
+ && (!excludeId.HasValue || x.Id != excludeId.Value),
+ cancellationToken);
+ }
+}