| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464 |
- using Admin.NET.Core;
- using Admin.NET.Core.Service;
- using Admin.NET.Plugin.AiDOP.DataPlatform.Inbound;
- using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
- using Yitter.IdGenerator;
- namespace Admin.NET.Plugin.AiDOP.DataPlatform;
- /// <summary>
- /// 来源映射维护:归属、库位角色、岗位、事务类型、来源能力、推送授权。
- /// 页面写入库位角色和岗位时 role_source 固定为 MANUAL,避免被自动分类冲掉。
- /// </summary>
- [ApiDescriptionSettings(Order = 326, Description = "来源映射维护")]
- [Route("api/DataPlatform")]
- [NonUnify]
- public class MdpSourceMappingService : IDynamicApiController, ITransient
- {
- private readonly ISqlSugarClient _db;
- private readonly UserManager _user;
- private readonly SysCacheService _cache;
- public MdpSourceMappingService(ISqlSugarClient db, UserManager user, SysCacheService cache)
- {
- _db = db;
- _user = user;
- _cache = cache;
- }
- private long ScopeTenant(long? requested)
- {
- if (_user.SuperAdmin || _user.SysAdmin)
- return requested is > 0 ? requested.Value : _user.TenantId;
- if (requested is > 0 && requested.Value != _user.TenantId)
- throw Oops.Oh("不能查看或修改其他租户的对接配置");
- return _user.TenantId;
- }
- [HttpGet("source-domain-maps")]
- public Task<List<dynamic>> ListDomainMaps([FromQuery] long? tenantId)
- {
- var tenant = ScopeTenant(tenantId);
- return _db.Ado.SqlQueryAsync<dynamic>(
- """
- SELECT id, system_code AS systemCode, source_code AS sourceCode, domain, tenant_id AS tenantId, status, remark
- FROM ado_source_domain_tenant_map
- WHERE tenant_id=@tenant
- ORDER BY system_code, domain
- """,
- new { tenant });
- }
- [HttpPut("source-domain-maps")]
- public async Task<object> SaveDomainMap([FromBody] DomainMapInput input)
- {
- var tenant = ScopeTenant(input.TenantId);
- if (string.IsNullOrWhiteSpace(input.SystemCode) || string.IsNullOrWhiteSpace(input.Domain))
- throw Oops.Oh("systemCode 与 domain 必填");
- var clash = await _db.Ado.SqlQueryAsync<long>(
- """
- SELECT tenant_id FROM ado_source_domain_tenant_map
- WHERE system_code=@sys AND domain=@dom AND status=1 AND tenant_id<>@tenant
- LIMIT 1
- """,
- new { sys = input.SystemCode.Trim(), dom = input.Domain.Trim(), tenant });
- if (clash.Count > 0)
- throw Oops.Oh($"该来源范围已归属租户 {clash[0]}。多个租户共享同一范围时按库位划分,不要改这里。");
- await _db.Ado.ExecuteCommandAsync(
- """
- INSERT INTO ado_source_domain_tenant_map (system_code, source_code, domain, tenant_id, status, remark)
- VALUES (@sys, @src, @dom, @tenant, 1, @remark)
- ON DUPLICATE KEY UPDATE source_code=VALUES(source_code), status=1, remark=VALUES(remark), tenant_id=VALUES(tenant_id)
- """,
- new
- {
- sys = input.SystemCode.Trim(),
- src = string.IsNullOrWhiteSpace(input.SourceCode) ? input.SystemCode.Trim() : input.SourceCode.Trim(),
- dom = input.Domain.Trim(),
- tenant,
- remark = input.Remark
- });
- return new { ok = true };
- }
- [HttpGet("location-roles")]
- public Task<List<dynamic>> ListLocations([FromQuery] long? tenantId, [FromQuery] string? domain, [FromQuery] bool unknownOnly = false)
- {
- var tenant = ScopeTenant(tenantId);
- return _db.Ado.SqlQueryAsync<dynamic>(
- """
- SELECT id, tenant_id AS tenantId, domain, location, location_role AS locationRole, role_source AS roleSource, remark
- FROM mdp_location_role
- WHERE tenant_id=@tenant
- AND (@dom = '' OR domain=@dom)
- AND (@unk = 0 OR location_role='UNKNOWN')
- ORDER BY domain, location
- LIMIT 500
- """,
- new { tenant, dom = domain?.Trim() ?? "", unk = unknownOnly ? 1 : 0 });
- }
- [HttpPut("location-roles")]
- public async Task<object> SaveLocations([FromBody] LocationRoleInput input)
- {
- var tenant = ScopeTenant(input.TenantId);
- if (input.Rows == null || input.Rows.Count == 0)
- throw Oops.Oh("没有要保存的行");
- var n = 0;
- foreach (var row in input.Rows)
- {
- if (string.IsNullOrWhiteSpace(row.Location) || string.IsNullOrWhiteSpace(row.LocationRole))
- continue;
- // 库位角色写错不会报错,只会让 LocationRules 的组合永远判不出阶段码,流水静默落进未映射隔离。
- var role = NeutralTransTypeCodes.NormalizeLocationRole(row.LocationRole)
- ?? throw Oops.Oh($"库位角色 {row.LocationRole} 不合法,只能是 {string.Join(" / ", NeutralTransTypeCodes.AllLocationRoles)}");
- n += await _db.Ado.ExecuteCommandAsync(
- """
- INSERT INTO mdp_location_role (tenant_id, domain, location, location_role, role_source, remark)
- VALUES (@tenant, @dom, @loc, @role, 'MANUAL', @remark)
- ON DUPLICATE KEY UPDATE location_role=VALUES(location_role), role_source='MANUAL', remark=VALUES(remark)
- """,
- new
- {
- tenant,
- dom = row.Domain?.Trim() ?? "",
- loc = row.Location.Trim(),
- role,
- remark = row.Remark
- });
- }
- return new { saved = n };
- }
- [HttpGet("position-maps")]
- public Task<List<dynamic>> ListPositions([FromQuery] long? tenantId, [FromQuery] bool unknownOnly = false)
- {
- var tenant = ScopeTenant(tenantId);
- return _db.Ado.SqlQueryAsync<dynamic>(
- """
- SELECT id, tenant_id AS tenantId, source_system AS sourceSystem, domain,
- src_position_raw AS srcPositionRaw, src_position_field AS srcPositionField,
- position_code AS positionCode, role_source AS roleSource, remark
- FROM mdp_employee_position_map
- WHERE tenant_id=@tenant AND (@unk = 0 OR position_code='UNKNOWN')
- ORDER BY source_system, src_position_raw
- LIMIT 500
- """,
- new { tenant, unk = unknownOnly ? 1 : 0 });
- }
- [HttpPut("position-maps")]
- public async Task<object> SavePositions([FromBody] PositionMapInput input)
- {
- var tenant = ScopeTenant(input.TenantId);
- if (input.Rows == null || input.Rows.Count == 0)
- throw Oops.Oh("没有要保存的行");
- var n = 0;
- foreach (var row in input.Rows)
- {
- if (string.IsNullOrWhiteSpace(row.SourceSystem) || string.IsNullOrWhiteSpace(row.SrcPositionRaw) || string.IsNullOrWhiteSpace(row.PositionCode))
- continue;
- // 岗位码写错不会报错,只会变成永远匹配不上 KPI 等值过滤的死值,故在入库前挡掉并归一化大小写。
- var code = EmployeePositionRules.Normalize(row.PositionCode)
- ?? throw Oops.Oh($"岗位码 {row.PositionCode} 不合法,只能是 {string.Join(" / ", EmployeePositionRules.AllowedCodes)}");
- n += await _db.Ado.ExecuteCommandAsync(
- """
- INSERT INTO mdp_employee_position_map
- (tenant_id, source_system, domain, src_position_raw, src_position_field, position_code, role_source, remark)
- VALUES (@tenant, @sys, @dom, @raw, @field, @code, 'MANUAL', @remark)
- ON DUPLICATE KEY UPDATE position_code=VALUES(position_code), role_source='MANUAL', remark=VALUES(remark)
- """,
- new
- {
- tenant,
- sys = row.SourceSystem.Trim(),
- dom = row.Domain?.Trim() ?? "",
- raw = row.SrcPositionRaw.Trim(),
- field = string.IsNullOrWhiteSpace(row.SrcPositionField) ? "Position" : row.SrcPositionField.Trim(),
- code,
- remark = row.Remark
- });
- }
- return new { saved = n };
- }
- [HttpGet("trans-type-maps")]
- public Task<List<dynamic>> ListTransTypes([FromQuery] string? systemCode)
- {
- return _db.Ado.SqlQueryAsync<dynamic>(
- """
- SELECT id, system_code AS systemCode, src_trans_code AS srcTransCode, qty_sign AS qtySign,
- location_role AS locationRole, stage_code AS stageCode, biz_doc_type AS bizDocType, is_stage AS isStage
- FROM mdp_trans_type_map
- WHERE tenant_id=0 AND (@sys = '' OR system_code=@sys)
- ORDER BY system_code, src_trans_code
- LIMIT 500
- """,
- new { sys = systemCode?.Trim() ?? "" });
- }
- [HttpPut("trans-type-maps")]
- public async Task<object> SaveTransType([FromBody] TransTypeInput input)
- {
- if (!_user.SuperAdmin && !_user.SysAdmin)
- throw Oops.Oh("事务类型全局行只有管理员可以改");
- if (string.IsNullOrWhiteSpace(input.SystemCode) || string.IsNullOrWhiteSpace(input.SrcTransCode))
- throw Oops.Oh("systemCode 与 srcTransCode 必填");
- await _db.Ado.ExecuteCommandAsync(
- """
- INSERT INTO mdp_trans_type_map
- (tenant_id, system_code, src_trans_code, qty_sign, location_role, stage_code, biz_doc_type, is_stage, excluded_by_location)
- VALUES (0, @sys, @src, @sgn, @role, @stage, @biz, @isStage, 0)
- ON DUPLICATE KEY UPDATE stage_code=VALUES(stage_code), biz_doc_type=VALUES(biz_doc_type), is_stage=VALUES(is_stage)
- """,
- new
- {
- sys = input.SystemCode.Trim(),
- src = input.SrcTransCode.Trim(),
- sgn = input.QtySign,
- role = input.LocationRole?.Trim() ?? "",
- stage = input.StageCode,
- biz = input.BizDocType,
- isStage = input.IsStage
- });
- return new { ok = true };
- }
- [HttpGet("source-capabilities")]
- public Task<List<dynamic>> ListCapabilities([FromQuery] string systemCode)
- {
- if (string.IsNullOrWhiteSpace(systemCode))
- throw Oops.Oh("systemCode 必填");
- return _db.Ado.SqlQueryAsync<dynamic>(
- """
- SELECT semantic_code AS semanticCode, supported, reason, evidence
- FROM mdp_source_capability
- WHERE source_system=@sys
- ORDER BY semantic_code
- """,
- new { sys = systemCode.Trim() });
- }
- [HttpPut("source-capabilities")]
- public async Task<object> SaveCapability([FromBody] CapabilityInput input)
- {
- if (string.IsNullOrWhiteSpace(input.SystemCode) || string.IsNullOrWhiteSpace(input.SemanticCode))
- throw Oops.Oh("systemCode 与 semanticCode 必填");
- await _db.Ado.ExecuteCommandAsync(
- """
- INSERT INTO mdp_source_capability (source_system, semantic_code, supported, reason, evidence, verified_at)
- VALUES (@sys, @code, @sup, @reason, @evidence, NOW())
- ON DUPLICATE KEY UPDATE supported=VALUES(supported), reason=VALUES(reason), evidence=VALUES(evidence), verified_at=NOW()
- """,
- new
- {
- sys = input.SystemCode.Trim(),
- code = input.SemanticCode.Trim(),
- sup = input.Supported ? 1 : 0,
- reason = input.Reason ?? "",
- evidence = input.Evidence
- });
- return new { ok = true };
- }
- [HttpGet("inbound-grants")]
- public async Task<List<dynamic>> ListGrants([FromQuery] long? tenantId, [FromQuery] string? sourceCode)
- {
- var tenant = ScopeTenant(tenantId);
- var rows = await _db.Ado.SqlQueryAsync<dynamic>(
- """
- SELECT id, entity_code AS entityCode, source_code AS sourceCode, rate_limit_per_min AS rateLimitPerMin,
- status, CONCAT(LEFT(access_key, 4), '…') AS accessKeyMask
- FROM mdp_inbound_grant
- WHERE tenant_id=@tenant AND (@src = '' OR source_code=@src)
- ORDER BY source_code, entity_code
- """,
- new { tenant, src = sourceCode?.Trim() ?? "" });
- return rows;
- }
- [HttpGet("required-columns")]
- public object RequiredColumns([FromQuery] string stdObject)
- {
- var def = MdpStdObjectCatalog.Find(stdObject);
- if (def == null)
- throw Oops.Oh($"未知标准对象 {stdObject}");
- return new
- {
- stdObject = def.Code,
- targetTable = def.Tables.FirstOrDefault(),
- columns = NeutralRequiredColumns.For(def.Code).Select(c => new { name = c.Name, semanticCode = c.SemanticCode })
- };
- }
- [HttpGet("semantic-codes")]
- public object SemanticCodes() =>
- Admin.NET.Plugin.AiDOP.SmartOps.KpiSemanticDependency.DistinctSemantics();
- [HttpPost("inbound-grants")]
- public async Task<object> CreateGrant([FromBody] GrantCreateInput input)
- {
- var tenant = ScopeTenant(input.TenantId);
- if (string.IsNullOrWhiteSpace(input.SourceCode) || string.IsNullOrWhiteSpace(input.EntityCode))
- throw Oops.Oh("sourceCode 与 entityCode 必填");
- var source = await _db.Queryable<MdpSource>()
- .Where(s => s.SourceCode == input.SourceCode.Trim())
- .FirstAsync() ?? throw Oops.Oh("来源不存在");
- if (!string.Equals(source.SourceType, "API_INBOUND", StringComparison.OrdinalIgnoreCase))
- throw Oops.Oh("只有对方推送来源可以发授权");
- var requestedKey = input.AccessKey?.Trim() ?? "";
- var identity = string.IsNullOrEmpty(requestedKey)
- ? null
- : await _db.Queryable<SysOpenAccess>().FirstAsync(x => x.AccessKey == requestedKey);
- var plan = InboundGrantIssuance.Plan(
- requestedKey,
- tenant,
- identity?.BindTenantId ?? 0,
- identity != null,
- NewToken,
- NewToken);
- var entityCode = input.EntityCode.Trim();
- switch (plan.Kind)
- {
- case InboundGrantIssuanceKind.RejectUnknownKey:
- throw Oops.Oh("访问标识不存在。留空将新建一套凭据,或先在开放接口身份中建立该标识");
- case InboundGrantIssuanceKind.RejectCrossTenant:
- throw Oops.Oh("该访问标识属于其他租户,不能用于本租户的推送授权");
- }
- var duplicated = await _db.Queryable<MdpInboundGrant>()
- .AnyAsync(x => x.AccessKey == plan.AccessKey && x.EntityCode == entityCode);
- if (duplicated)
- throw Oops.Oh("该访问标识已经授权过这个业务对象");
- long bindUserId = 0;
- if (plan.Kind == InboundGrantIssuanceKind.CreateIdentity)
- {
- bindUserId = await _db.Queryable<SysTenant>()
- .Where(t => t.Id == tenant)
- .Select(t => t.UserId)
- .FirstAsync();
- if (bindUserId <= 0)
- throw Oops.Oh("租户尚未生成租管用户,无法签发推送身份");
- }
- var now = DateTime.Now;
- try
- {
- _db.Ado.BeginTran();
- if (plan.Kind == InboundGrantIssuanceKind.CreateIdentity)
- {
- await _db.Insertable(new SysOpenAccess
- {
- Id = YitIdHelper.NextId(),
- AccessKey = plan.AccessKey,
- AccessSecret = plan.AccessSecret,
- BindTenantId = tenant,
- BindUserId = bindUserId,
- CreateTime = now,
- }).ExecuteCommandAsync();
- }
- await _db.Insertable(new MdpInboundGrant
- {
- TenantId = tenant,
- AccessKey = plan.AccessKey,
- EntityCode = entityCode,
- SourceCode = source.SourceCode,
- RateLimitPerMin = input.RateLimitPerMin <= 0 ? 60 : input.RateLimitPerMin,
- BatchRowLimit = 500,
- Status = 1,
- CreateTime = now,
- UpdateTime = now
- }).ExecuteCommandAsync();
- _db.Ado.CommitTran();
- }
- catch
- {
- _db.Ado.RollbackTran();
- throw;
- }
- if (plan.Kind == InboundGrantIssuanceKind.CreateIdentity)
- _cache.Remove(CacheConst.KeyOpenAccess + plan.AccessKey);
- // 密钥只在新建时回显一次。复用已有标识时调用方本就持有密钥,不再返回。
- return plan.Kind == InboundGrantIssuanceKind.CreateIdentity
- ? new { accessKey = plan.AccessKey, accessSecret = plan.AccessSecret, entityCode, secretShownOnce = true }
- : (object)new { accessKey = plan.AccessKey, entityCode, secretShownOnce = false };
- }
- private static string NewToken() =>
- Convert.ToHexString(System.Security.Cryptography.RandomNumberGenerator.GetBytes(16)).ToLowerInvariant();
- }
- public sealed class DomainMapInput
- {
- public long TenantId { get; set; }
- public string SystemCode { get; set; } = "";
- public string? SourceCode { get; set; }
- public string Domain { get; set; } = "";
- public string? Remark { get; set; }
- }
- public sealed class LocationRoleInput
- {
- public long TenantId { get; set; }
- public List<LocationRoleRow> Rows { get; set; } = [];
- }
- public sealed class LocationRoleRow
- {
- public string? Domain { get; set; }
- public string Location { get; set; } = "";
- public string LocationRole { get; set; } = "";
- public string? Remark { get; set; }
- }
- public sealed class PositionMapInput
- {
- public long TenantId { get; set; }
- public List<PositionMapRow> Rows { get; set; } = [];
- }
- public sealed class PositionMapRow
- {
- public string SourceSystem { get; set; } = "";
- public string? Domain { get; set; }
- public string SrcPositionRaw { get; set; } = "";
- public string? SrcPositionField { get; set; }
- public string PositionCode { get; set; } = "";
- public string? Remark { get; set; }
- }
- public sealed class TransTypeInput
- {
- public string SystemCode { get; set; } = "";
- public string SrcTransCode { get; set; } = "";
- public int QtySign { get; set; }
- public string? LocationRole { get; set; }
- public string? StageCode { get; set; }
- public string? BizDocType { get; set; }
- public int IsStage { get; set; } = 1;
- }
- public sealed class CapabilityInput
- {
- public string SystemCode { get; set; } = "";
- public string SemanticCode { get; set; } = "";
- public bool Supported { get; set; }
- public string? Reason { get; set; }
- public string? Evidence { get; set; }
- }
- public sealed class GrantCreateInput
- {
- public long TenantId { get; set; }
- public string SourceCode { get; set; } = "";
- public string EntityCode { get; set; } = "";
- public int RateLimitPerMin { get; set; } = 60;
- /// <summary>留空则新建开放接口身份并回显密钥;传入则复用该身份,且必须属于目标租户。</summary>
- public string AccessKey { get; set; }
- }
|