- 新增 Finance 模块应收应付分页查询及订单费用联查 - 为 DataPerm、Xhs 等缺失接口补充中文 summary 接口名称 - 优化微信公众号验签 Swagger 命名并补充路由集成测试 Co-authored-by: Cursor <cursoragent@cursor.com>
410 lines
16 KiB
C#
410 lines
16 KiB
C#
using Fake.Application;
|
|
using Oms.Application.DataPerm;
|
|
using System.Security.Claims;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Oms.Application.Dtos.DataPerm;
|
|
using Oms.Application.Shared;
|
|
using System.Text.Json;
|
|
|
|
namespace Oms.Application.Services.DataPerm;
|
|
|
|
public partial class DataPermService
|
|
{
|
|
/// <summary>
|
|
/// 分页查询数据权限模板
|
|
/// </summary>
|
|
[HttpGet("/rbac/data-perm/template/list")]
|
|
public Task<DataPermPagedResult<DataPermTemplateListItemDto>> GetTemplateListAsync(
|
|
[FromQuery] string? keyword,
|
|
[FromQuery] int page = 1,
|
|
[FromQuery] int pageSize = 30,
|
|
CancellationToken cancellationToken = default) =>
|
|
Store.GetTemplateListAsync(
|
|
keyword,
|
|
OmsDocHelper.NormalizePage(page),
|
|
OmsDocHelper.NormalizePageSize(pageSize),
|
|
cancellationToken);
|
|
|
|
/// <summary>
|
|
/// 查询数据权限模板详情
|
|
/// </summary>
|
|
[HttpGet("/rbac/data-perm/template/get")]
|
|
public Task<DataPermTemplateDetailDto> GetTemplateDetailAsync(
|
|
[FromQuery] string id,
|
|
CancellationToken cancellationToken = default) =>
|
|
Store.GetTemplateDetailAsync(ParseGuid(id, "id"), cancellationToken);
|
|
|
|
/// <summary>
|
|
/// 创建数据权限模板
|
|
/// </summary>
|
|
[HttpPost("/rbac/data-perm/template/create")]
|
|
public Task<DataPermTemplateDetailDto> CreateTemplateAsync(
|
|
[FromBody] DataPermTemplateSaveInput input,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ValidateTemplateInput(input);
|
|
return Store.CreateTemplateAsync(input, GetCurrentMapId(), cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 更新数据权限模板
|
|
/// </summary>
|
|
[HttpPut("/rbac/data-perm/template/update")]
|
|
public Task<DataPermTemplateDetailDto> UpdateTemplateAsync(
|
|
[FromQuery] string id,
|
|
[FromBody] DataPermTemplateSaveInput input,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ValidateTemplateInput(input);
|
|
return Store.UpdateTemplateAsync(ParseGuid(id, "id"), input, GetCurrentMapId(), cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 删除数据权限模板
|
|
/// </summary>
|
|
[HttpDelete("/rbac/data-perm/template/delete")]
|
|
public Task DeleteTemplateAsync([FromQuery] string id, CancellationToken cancellationToken = default) =>
|
|
Store.DeleteTemplateAsync(ParseGuid(id, "id"), cancellationToken);
|
|
|
|
/// <summary>
|
|
/// 应用数据权限模板
|
|
/// </summary>
|
|
[HttpPost("/rbac/data-perm/template/apply")]
|
|
public async Task<TemplateApplyResultDto> ApplyTemplateAsync(
|
|
[FromBody] TemplateApplyInput input,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var template = await Store.GetTemplateDetailAsync(ParseGuid(input.TemplateId, "templateId"), cancellationToken);
|
|
var bundle = TemplateBundleParser.Parse(template.Items);
|
|
var result = new TemplateApplyResultDto();
|
|
|
|
foreach (var targetIdRaw in input.TargetIds.Distinct(StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
try
|
|
{
|
|
if (string.Equals(input.TargetType, "role", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var roleId = ParseGuid(targetIdRaw, "targetId");
|
|
var current = await Store.GetRoleDataPermAsync(roleId, cancellationToken);
|
|
var merged = MergeRoleBundle(current, bundle, input);
|
|
await Store.SaveRoleDataPermAsync(roleId, merged, GetCurrentUserGuid(), GetCurrentMapId(), Repository, cancellationToken);
|
|
}
|
|
else
|
|
{
|
|
var userId = ParseGuid(targetIdRaw, "targetId");
|
|
if (await Repository.IsPureCustomerUserAsync(userId, cancellationToken))
|
|
{
|
|
throw new BusinessException("客户账号不支持数据权限配置");
|
|
}
|
|
|
|
var current = await Store.GetUserDataPermAsync(userId, cancellationToken);
|
|
var merged = MergeUserBundle(current, bundle, input);
|
|
DataPermSaveValidator.Validate(merged, Repository);
|
|
await DataPermSaveValidator.ValidateOrganizationsExistAsync(merged, Repository, cancellationToken);
|
|
await Store.SaveUserDataPermAsync(userId, merged, GetCurrentUserGuid(), GetCurrentMapId(), cancellationToken);
|
|
}
|
|
|
|
result.SuccessCount++;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.Failures.Add(new TemplateApplyFailureDto
|
|
{
|
|
TargetId = targetIdRaw,
|
|
Reason = ex is BusinessException business ? business.Message : ex.Message
|
|
});
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static void ValidateTemplateInput(DataPermTemplateSaveInput input)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(input.Name) || string.IsNullOrWhiteSpace(input.Code))
|
|
{
|
|
throw new BusinessException("模板名称与编码不能为空");
|
|
}
|
|
}
|
|
|
|
private static SaveUserDataPermInput MergeUserBundle(
|
|
UserDataPermDto current,
|
|
TemplatePermBundle bundle,
|
|
TemplateApplyInput input)
|
|
{
|
|
var replace = string.Equals(input.Mode, "replace", StringComparison.OrdinalIgnoreCase);
|
|
var effectiveTime = input.Temporary?.Enabled == true ? input.Temporary.EffectiveTime : null;
|
|
var expireTime = input.Temporary?.Enabled == true ? input.Temporary.ExpireTime : null;
|
|
var isTemporary = input.Temporary?.Enabled == true;
|
|
|
|
var orgPerms = replace
|
|
? bundle.OrgPerms
|
|
: UnionOrg(current.OrgPerms, bundle.OrgPerms);
|
|
var dataScopes = replace
|
|
? bundle.DataScopes
|
|
: MergeDataScopes(current.DataScopes, bundle.DataScopes);
|
|
var bizPerms = replace
|
|
? bundle.BizPerms
|
|
: UnionBiz(current.BizPerms, bundle.BizPerms);
|
|
var secretPerms = replace
|
|
? bundle.SecretPerms
|
|
: UnionSecret(current.SecretPerms, bundle.SecretPerms);
|
|
var filterRules = replace
|
|
? bundle.FilterRules
|
|
: current.FilterRules.Select(ToFilterSave).Concat(bundle.FilterRules).ToList();
|
|
|
|
ApplyTemporary(orgPerms, effectiveTime, expireTime, isTemporary);
|
|
|
|
return new SaveUserDataPermInput
|
|
{
|
|
OrgPerms = orgPerms,
|
|
DataScopes = dataScopes,
|
|
BizPerms = bizPerms,
|
|
SecretPerms = secretPerms,
|
|
FilterRules = filterRules
|
|
};
|
|
}
|
|
|
|
private static SaveUserDataPermInput MergeRoleBundle(
|
|
RoleDataPermDto current,
|
|
TemplatePermBundle bundle,
|
|
TemplateApplyInput input)
|
|
{
|
|
var replace = string.Equals(input.Mode, "replace", StringComparison.OrdinalIgnoreCase);
|
|
return new SaveUserDataPermInput
|
|
{
|
|
OrgPerms = replace ? bundle.OrgPerms : UnionOrg(current.OrgPerms, bundle.OrgPerms),
|
|
DataScopes = replace ? bundle.DataScopes : MergeDataScopes(current.DataScopes, bundle.DataScopes),
|
|
BizPerms = replace ? bundle.BizPerms : UnionBiz(current.BizPerms, bundle.BizPerms),
|
|
SecretPerms = replace ? bundle.SecretPerms : UnionSecret(current.SecretPerms, bundle.SecretPerms),
|
|
FilterRules = replace ? bundle.FilterRules : current.FilterRules.Select(ToFilterSave).Concat(bundle.FilterRules).ToList()
|
|
};
|
|
}
|
|
|
|
private static List<OrgPermSaveItemDto> UnionOrg(IEnumerable<OrgPermItemDto> existing, List<OrgPermSaveItemDto> incoming)
|
|
{
|
|
var map = existing.ToDictionary(x => x.OrganizationId, ToOrgSave, StringComparer.OrdinalIgnoreCase);
|
|
foreach (var item in incoming)
|
|
{
|
|
map[item.OrganizationId] = item;
|
|
}
|
|
|
|
return map.Values.ToList();
|
|
}
|
|
|
|
private static List<BizPermSaveItemDto> UnionBiz(IEnumerable<BizPermItemDto> existing, List<BizPermSaveItemDto> incoming)
|
|
{
|
|
var map = existing.ToDictionary(x => $"{x.BizType}:{x.BizId}", ToBizSave, StringComparer.OrdinalIgnoreCase);
|
|
foreach (var item in incoming)
|
|
{
|
|
map[$"{item.BizType}:{item.BizId}"] = item;
|
|
}
|
|
|
|
return map.Values.ToList();
|
|
}
|
|
|
|
private static List<SecretPermSaveItemDto> UnionSecret(IEnumerable<SecretPermItemDto> existing, List<SecretPermSaveItemDto> incoming)
|
|
{
|
|
var set = existing.Select(x => (x.SecretLevel, x.DataResourceCode ?? string.Empty)).ToHashSet();
|
|
var result = existing.Select(ToSecretSave).ToList();
|
|
foreach (var item in incoming)
|
|
{
|
|
var key = (item.SecretLevel, item.DataResourceCode ?? string.Empty);
|
|
if (set.Add(key))
|
|
{
|
|
result.Add(item);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static List<DataScopeSaveItemDto> MergeDataScopes(
|
|
IEnumerable<DataScopeItemDto> existing,
|
|
List<DataScopeSaveItemDto> incoming)
|
|
{
|
|
var map = existing.ToDictionary(x => x.DataResourceCode ?? string.Empty, ToScopeSave, StringComparer.OrdinalIgnoreCase);
|
|
foreach (var item in incoming)
|
|
{
|
|
var key = item.DataResourceCode ?? string.Empty;
|
|
if (!map.TryGetValue(key, out var current) || item.DataScope > current.DataScope)
|
|
{
|
|
map[key] = item;
|
|
}
|
|
}
|
|
|
|
return map.Values.ToList();
|
|
}
|
|
|
|
private static void ApplyTemporary(
|
|
List<OrgPermSaveItemDto> orgPerms,
|
|
DateTime? effectiveTime,
|
|
DateTime? expireTime,
|
|
bool isTemporary)
|
|
{
|
|
if (!isTemporary)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var item in orgPerms)
|
|
{
|
|
item.EffectiveTime = effectiveTime ?? item.EffectiveTime;
|
|
item.ExpireTime = expireTime ?? item.ExpireTime;
|
|
item.IsTemporary = true;
|
|
}
|
|
}
|
|
|
|
private static OrgPermSaveItemDto ToOrgSave(OrgPermItemDto item) => new()
|
|
{
|
|
OrganizationId = item.OrganizationId,
|
|
OrgScope = item.OrgScope,
|
|
EffectiveTime = item.EffectiveTime,
|
|
ExpireTime = item.ExpireTime,
|
|
IsTemporary = item.IsTemporary
|
|
};
|
|
|
|
private static DataScopeSaveItemDto ToScopeSave(DataScopeItemDto item) => new()
|
|
{
|
|
DataScope = item.DataScope,
|
|
DataResourceCode = item.DataResourceCode,
|
|
ExpireTime = item.ExpireTime
|
|
};
|
|
|
|
private static BizPermSaveItemDto ToBizSave(BizPermItemDto item) => new()
|
|
{
|
|
BizType = item.BizType,
|
|
BizId = item.BizId,
|
|
BizName = item.BizName,
|
|
ExpireTime = item.ExpireTime
|
|
};
|
|
|
|
private static SecretPermSaveItemDto ToSecretSave(SecretPermItemDto item) => new()
|
|
{
|
|
SecretLevel = item.SecretLevel,
|
|
DataResourceCode = item.DataResourceCode,
|
|
ExpireTime = item.ExpireTime
|
|
};
|
|
|
|
private static FilterRuleSaveItemDto ToFilterSave(FilterRuleItemDto item) => new()
|
|
{
|
|
DataResourceCode = item.DataResourceCode,
|
|
FilterContent = item.FilterContent,
|
|
ExpireTime = item.ExpireTime
|
|
};
|
|
|
|
private sealed class TemplatePermBundle
|
|
{
|
|
public List<OrgPermSaveItemDto> OrgPerms { get; init; } = [];
|
|
|
|
public List<DataScopeSaveItemDto> DataScopes { get; init; } = [];
|
|
|
|
public List<BizPermSaveItemDto> BizPerms { get; init; } = [];
|
|
|
|
public List<SecretPermSaveItemDto> SecretPerms { get; init; } = [];
|
|
|
|
public List<FilterRuleSaveItemDto> FilterRules { get; init; } = [];
|
|
}
|
|
|
|
private static class TemplateBundleParser
|
|
{
|
|
public static TemplatePermBundle Parse(IEnumerable<DataPermTemplateItemDto> items)
|
|
{
|
|
var bundle = new TemplatePermBundle();
|
|
foreach (var item in items)
|
|
{
|
|
switch (item.DimensionType.ToLowerInvariant())
|
|
{
|
|
case "org":
|
|
bundle.OrgPerms.Add(ParseOrg(item.ItemContent));
|
|
break;
|
|
case "data_scope":
|
|
bundle.DataScopes.Add(ParseDataScope(item.ItemContent));
|
|
break;
|
|
case "biz":
|
|
bundle.BizPerms.Add(ParseBiz(item.ItemContent));
|
|
break;
|
|
case "secret":
|
|
bundle.SecretPerms.AddRange(ParseSecret(item.ItemContent));
|
|
break;
|
|
case "filter":
|
|
bundle.FilterRules.Add(ParseFilter(item.ItemContent));
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bundle;
|
|
}
|
|
|
|
private static OrgPermSaveItemDto ParseOrg(Dictionary<string, object?> content) => new()
|
|
{
|
|
OrganizationId = GetString(content, "organizationId"),
|
|
OrgScope = GetInt(content, "orgScope", DataPermConstants.OrgScopeSelfAndChildren)
|
|
};
|
|
|
|
private static DataScopeSaveItemDto ParseDataScope(Dictionary<string, object?> content) => new()
|
|
{
|
|
DataScope = GetInt(content, "dataScope", DataPermConstants.DataScopePersonal),
|
|
DataResourceCode = GetOptionalString(content, "dataResourceCode")
|
|
};
|
|
|
|
private static BizPermSaveItemDto ParseBiz(Dictionary<string, object?> content) => new()
|
|
{
|
|
BizType = GetString(content, "bizType"),
|
|
BizId = GetString(content, "bizId"),
|
|
BizName = GetOptionalString(content, "bizName")
|
|
};
|
|
|
|
private static IEnumerable<SecretPermSaveItemDto> ParseSecret(Dictionary<string, object?> content)
|
|
{
|
|
if (content.TryGetValue("secretLevels", out var raw) && raw is JsonElement element && element.ValueKind == JsonValueKind.Array)
|
|
{
|
|
return element.EnumerateArray().Select(x => new SecretPermSaveItemDto
|
|
{
|
|
SecretLevel = x.GetInt32(),
|
|
DataResourceCode = GetOptionalString(content, "dataResourceCode")
|
|
});
|
|
}
|
|
|
|
return
|
|
[
|
|
new SecretPermSaveItemDto
|
|
{
|
|
SecretLevel = GetInt(content, "secretLevel", 1),
|
|
DataResourceCode = GetOptionalString(content, "dataResourceCode")
|
|
}
|
|
];
|
|
}
|
|
|
|
private static FilterRuleSaveItemDto ParseFilter(Dictionary<string, object?> content)
|
|
{
|
|
var resourceCode = GetString(content, "dataResourceCode");
|
|
FilterContentDto filterContent;
|
|
if (content.TryGetValue("filterContent", out var raw) && raw is JsonElement element)
|
|
{
|
|
filterContent = element.Deserialize<FilterContentDto>() ?? new FilterContentDto();
|
|
}
|
|
else
|
|
{
|
|
filterContent = new FilterContentDto();
|
|
}
|
|
|
|
FilterAstValidator.ParseAndValidate(resourceCode, filterContent);
|
|
return new FilterRuleSaveItemDto
|
|
{
|
|
DataResourceCode = resourceCode,
|
|
FilterContent = filterContent
|
|
};
|
|
}
|
|
|
|
private static string GetString(Dictionary<string, object?> content, string key) =>
|
|
content.TryGetValue(key, out var raw) ? raw?.ToString()?.Trim() ?? string.Empty : string.Empty;
|
|
|
|
private static string? GetOptionalString(Dictionary<string, object?> content, string key) =>
|
|
content.TryGetValue(key, out var raw) ? raw?.ToString()?.Trim() : null;
|
|
|
|
private static int GetInt(Dictionary<string, object?> content, string key, int defaultValue) =>
|
|
content.TryGetValue(key, out var raw) && int.TryParse(raw?.ToString(), out var value) ? value : defaultValue;
|
|
}
|
|
}
|