feat(channel): 新增问题模板 CRUD 与 options API
支持按组织与问题类型维护通知模板,含建表脚本与仓储层唯一性校验。
This commit is contained in:
parent
cde0ccb6b0
commit
d49573fe9a
19
db.sql
19
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_outbound_record`;
|
||||||
DROP TABLE IF EXISTS `t_inbound_record`;
|
DROP TABLE IF EXISTS `t_inbound_record`;
|
||||||
DROP TABLE IF EXISTS `t_label_template`;
|
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_auto_trigger`;
|
||||||
DROP TABLE IF EXISTS `t_weight_rounding_info`;
|
DROP TABLE IF EXISTS `t_weight_rounding_info`;
|
||||||
DROP TABLE IF EXISTS `t_weight_rounding`;
|
DROP TABLE IF EXISTS `t_weight_rounding`;
|
||||||
@ -653,6 +654,24 @@ CREATE TABLE `t_auto_trigger` (
|
|||||||
KEY `idx_at_operation` (`Operation`)
|
KEY `idx_at_operation` (`Operation`)
|
||||||
) COMMENT='自动触发表';
|
) 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` (
|
CREATE TABLE `t_label_template` (
|
||||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1',
|
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1',
|
||||||
`Name` VARCHAR(128) NOT NULL COMMENT '模板名称',
|
`Name` VARCHAR(128) NOT NULL COMMENT '模板名称',
|
||||||
|
|||||||
18
sql/migrate_t_issue_template.sql
Normal file
18
sql/migrate_t_issue_template.sql
Normal file
@ -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='渠道-问题模板';
|
||||||
19
src/Oms.Application/Dtos/Channels/IssueTemplateListInput.cs
Normal file
19
src/Oms.Application/Dtos/Channels/IssueTemplateListInput.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
namespace Oms.Application.Dtos.Channels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 问题模板列表查询入参。
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
namespace Oms.Application.Dtos.Channels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 问题模板下拉选项。
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
31
src/Oms.Application/Dtos/Channels/IssueTemplateOutput.cs
Normal file
31
src/Oms.Application/Dtos/Channels/IssueTemplateOutput.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
namespace Oms.Application.Dtos.Channels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 问题模板出参。
|
||||||
|
/// </summary>
|
||||||
|
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<string> NotifyMethods { get; set; } = [];
|
||||||
|
|
||||||
|
public byte Status { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreateTime { get; set; }
|
||||||
|
|
||||||
|
public DateTime? UpdateTime { get; set; }
|
||||||
|
}
|
||||||
23
src/Oms.Application/Dtos/Channels/IssueTemplateSaveInput.cs
Normal file
23
src/Oms.Application/Dtos/Channels/IssueTemplateSaveInput.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using Oms.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
namespace Oms.Application.Dtos.Channels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 问题模板保存入参。
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
303
src/Oms.Application/Services/Channels/IssueTemplateService.cs
Normal file
303
src/Oms.Application/Services/Channels/IssueTemplateService.cs
Normal file
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 问题模板 API 服务。
|
||||||
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(GroupName = "OMS-Channel")]
|
||||||
|
public class IssueTemplateService : ApplicationService
|
||||||
|
{
|
||||||
|
private IIssueTemplateRepository IssueTemplateRepository =>
|
||||||
|
LazyServiceProvider.GetRequiredService<IIssueTemplateRepository>();
|
||||||
|
|
||||||
|
private DataPermRepository DataPermRepository =>
|
||||||
|
LazyServiceProvider.GetRequiredService<DataPermRepository>();
|
||||||
|
|
||||||
|
[HttpGet("/channel/issue-template/list")]
|
||||||
|
public async Task<OmsPageResult<IssueTemplateOutput>> 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<string, bool> { [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<IssueTemplateOutput> { Items = items, Total = total };
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("/channel/issue-template/detail")]
|
||||||
|
public async Task<IssueTemplateOutput> 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<OmsItemsResult<IssueTemplateOptionOutput>> 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<IssueTemplateOptionOutput> { Items = grouped };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Expression<Func<IssueTemplate, bool>> 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<NormalizedIssueTemplateSaveInput> 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<IssueTemplate> 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<Dictionary<Guid, string>> ResolveOrganizationNameMapAsync(
|
||||||
|
IReadOnlyCollection<IssueTemplate> 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<Guid, string> 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<string> 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<string> ParseList(string? raw)
|
||||||
|
{
|
||||||
|
var normalized = Normalize(raw);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/Oms.Domain/ChannelsAggregate/Consts/IssueTypeCodes.cs
Normal file
38
src/Oms.Domain/ChannelsAggregate/Consts/IssueTypeCodes.cs
Normal file
@ -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<string> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
21
src/Oms.Domain/ChannelsAggregate/Entities/IssueTemplate.cs
Normal file
21
src/Oms.Domain/ChannelsAggregate/Entities/IssueTemplate.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
using Fake.Domain.Repositories;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
|
|
||||||
|
namespace Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
|
|
||||||
|
public interface IIssueTemplateRepository : IRepository<IssueTemplate>
|
||||||
|
{
|
||||||
|
Task<bool> ExistsDuplicateAsync(
|
||||||
|
string? organizationId,
|
||||||
|
string issueType,
|
||||||
|
int? excludeId = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
27
src/Oms.SqlSugarCore/Channels/IssueTemplateRepository.cs
Normal file
27
src/Oms.SqlSugarCore/Channels/IssueTemplateRepository.cs
Normal file
@ -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<IssueTemplate>, IIssueTemplateRepository, IScopedDependency
|
||||||
|
{
|
||||||
|
public async Task<bool> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user