MdpSourceMappingService.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. using Admin.NET.Core;
  2. using Admin.NET.Core.Service;
  3. using Admin.NET.Plugin.AiDOP.DataPlatform.Inbound;
  4. using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
  5. using Yitter.IdGenerator;
  6. namespace Admin.NET.Plugin.AiDOP.DataPlatform;
  7. /// <summary>
  8. /// 来源映射维护:归属、库位角色、岗位、事务类型、来源能力、推送授权。
  9. /// 页面写入库位角色和岗位时 role_source 固定为 MANUAL,避免被自动分类冲掉。
  10. /// </summary>
  11. [ApiDescriptionSettings(Order = 326, Description = "来源映射维护")]
  12. [Route("api/DataPlatform")]
  13. [NonUnify]
  14. public class MdpSourceMappingService : IDynamicApiController, ITransient
  15. {
  16. private readonly ISqlSugarClient _db;
  17. private readonly UserManager _user;
  18. private readonly SysCacheService _cache;
  19. public MdpSourceMappingService(ISqlSugarClient db, UserManager user, SysCacheService cache)
  20. {
  21. _db = db;
  22. _user = user;
  23. _cache = cache;
  24. }
  25. private long ScopeTenant(long? requested)
  26. {
  27. if (_user.SuperAdmin || _user.SysAdmin)
  28. return requested is > 0 ? requested.Value : _user.TenantId;
  29. if (requested is > 0 && requested.Value != _user.TenantId)
  30. throw Oops.Oh("不能查看或修改其他租户的对接配置");
  31. return _user.TenantId;
  32. }
  33. [HttpGet("source-domain-maps")]
  34. public Task<List<dynamic>> ListDomainMaps([FromQuery] long? tenantId)
  35. {
  36. var tenant = ScopeTenant(tenantId);
  37. return _db.Ado.SqlQueryAsync<dynamic>(
  38. """
  39. SELECT id, system_code AS systemCode, source_code AS sourceCode, domain, tenant_id AS tenantId, status, remark
  40. FROM ado_source_domain_tenant_map
  41. WHERE tenant_id=@tenant
  42. ORDER BY system_code, domain
  43. """,
  44. new { tenant });
  45. }
  46. [HttpPut("source-domain-maps")]
  47. public async Task<object> SaveDomainMap([FromBody] DomainMapInput input)
  48. {
  49. var tenant = ScopeTenant(input.TenantId);
  50. if (string.IsNullOrWhiteSpace(input.SystemCode) || string.IsNullOrWhiteSpace(input.Domain))
  51. throw Oops.Oh("systemCode 与 domain 必填");
  52. var clash = await _db.Ado.SqlQueryAsync<long>(
  53. """
  54. SELECT tenant_id FROM ado_source_domain_tenant_map
  55. WHERE system_code=@sys AND domain=@dom AND status=1 AND tenant_id<>@tenant
  56. LIMIT 1
  57. """,
  58. new { sys = input.SystemCode.Trim(), dom = input.Domain.Trim(), tenant });
  59. if (clash.Count > 0)
  60. throw Oops.Oh($"该来源范围已归属租户 {clash[0]}。多个租户共享同一范围时按库位划分,不要改这里。");
  61. await _db.Ado.ExecuteCommandAsync(
  62. """
  63. INSERT INTO ado_source_domain_tenant_map (system_code, source_code, domain, tenant_id, status, remark)
  64. VALUES (@sys, @src, @dom, @tenant, 1, @remark)
  65. ON DUPLICATE KEY UPDATE source_code=VALUES(source_code), status=1, remark=VALUES(remark), tenant_id=VALUES(tenant_id)
  66. """,
  67. new
  68. {
  69. sys = input.SystemCode.Trim(),
  70. src = string.IsNullOrWhiteSpace(input.SourceCode) ? input.SystemCode.Trim() : input.SourceCode.Trim(),
  71. dom = input.Domain.Trim(),
  72. tenant,
  73. remark = input.Remark
  74. });
  75. return new { ok = true };
  76. }
  77. [HttpGet("location-roles")]
  78. public Task<List<dynamic>> ListLocations([FromQuery] long? tenantId, [FromQuery] string? domain, [FromQuery] bool unknownOnly = false)
  79. {
  80. var tenant = ScopeTenant(tenantId);
  81. return _db.Ado.SqlQueryAsync<dynamic>(
  82. """
  83. SELECT id, tenant_id AS tenantId, domain, location, location_role AS locationRole, role_source AS roleSource, remark
  84. FROM mdp_location_role
  85. WHERE tenant_id=@tenant
  86. AND (@dom = '' OR domain=@dom)
  87. AND (@unk = 0 OR location_role='UNKNOWN')
  88. ORDER BY domain, location
  89. LIMIT 500
  90. """,
  91. new { tenant, dom = domain?.Trim() ?? "", unk = unknownOnly ? 1 : 0 });
  92. }
  93. [HttpPut("location-roles")]
  94. public async Task<object> SaveLocations([FromBody] LocationRoleInput input)
  95. {
  96. var tenant = ScopeTenant(input.TenantId);
  97. if (input.Rows == null || input.Rows.Count == 0)
  98. throw Oops.Oh("没有要保存的行");
  99. var n = 0;
  100. foreach (var row in input.Rows)
  101. {
  102. if (string.IsNullOrWhiteSpace(row.Location) || string.IsNullOrWhiteSpace(row.LocationRole))
  103. continue;
  104. n += await _db.Ado.ExecuteCommandAsync(
  105. """
  106. INSERT INTO mdp_location_role (tenant_id, domain, location, location_role, role_source, remark)
  107. VALUES (@tenant, @dom, @loc, @role, 'MANUAL', @remark)
  108. ON DUPLICATE KEY UPDATE location_role=VALUES(location_role), role_source='MANUAL', remark=VALUES(remark)
  109. """,
  110. new
  111. {
  112. tenant,
  113. dom = row.Domain?.Trim() ?? "",
  114. loc = row.Location.Trim(),
  115. role = row.LocationRole.Trim(),
  116. remark = row.Remark
  117. });
  118. }
  119. return new { saved = n };
  120. }
  121. [HttpGet("position-maps")]
  122. public Task<List<dynamic>> ListPositions([FromQuery] long? tenantId, [FromQuery] bool unknownOnly = false)
  123. {
  124. var tenant = ScopeTenant(tenantId);
  125. return _db.Ado.SqlQueryAsync<dynamic>(
  126. """
  127. SELECT id, tenant_id AS tenantId, source_system AS sourceSystem, domain,
  128. src_position_raw AS srcPositionRaw, src_position_field AS srcPositionField,
  129. position_code AS positionCode, role_source AS roleSource, remark
  130. FROM mdp_employee_position_map
  131. WHERE tenant_id=@tenant AND (@unk = 0 OR position_code='UNKNOWN')
  132. ORDER BY source_system, src_position_raw
  133. LIMIT 500
  134. """,
  135. new { tenant, unk = unknownOnly ? 1 : 0 });
  136. }
  137. [HttpPut("position-maps")]
  138. public async Task<object> SavePositions([FromBody] PositionMapInput input)
  139. {
  140. var tenant = ScopeTenant(input.TenantId);
  141. if (input.Rows == null || input.Rows.Count == 0)
  142. throw Oops.Oh("没有要保存的行");
  143. var n = 0;
  144. foreach (var row in input.Rows)
  145. {
  146. if (string.IsNullOrWhiteSpace(row.SourceSystem) || string.IsNullOrWhiteSpace(row.SrcPositionRaw) || string.IsNullOrWhiteSpace(row.PositionCode))
  147. continue;
  148. n += await _db.Ado.ExecuteCommandAsync(
  149. """
  150. INSERT INTO mdp_employee_position_map
  151. (tenant_id, source_system, domain, src_position_raw, src_position_field, position_code, role_source, remark)
  152. VALUES (@tenant, @sys, @dom, @raw, @field, @code, 'MANUAL', @remark)
  153. ON DUPLICATE KEY UPDATE position_code=VALUES(position_code), role_source='MANUAL', remark=VALUES(remark)
  154. """,
  155. new
  156. {
  157. tenant,
  158. sys = row.SourceSystem.Trim(),
  159. dom = row.Domain?.Trim() ?? "",
  160. raw = row.SrcPositionRaw.Trim(),
  161. field = string.IsNullOrWhiteSpace(row.SrcPositionField) ? "Position" : row.SrcPositionField.Trim(),
  162. code = row.PositionCode.Trim(),
  163. remark = row.Remark
  164. });
  165. }
  166. return new { saved = n };
  167. }
  168. [HttpGet("trans-type-maps")]
  169. public Task<List<dynamic>> ListTransTypes([FromQuery] string? systemCode)
  170. {
  171. return _db.Ado.SqlQueryAsync<dynamic>(
  172. """
  173. SELECT id, system_code AS systemCode, src_trans_code AS srcTransCode, qty_sign AS qtySign,
  174. location_role AS locationRole, stage_code AS stageCode, biz_doc_type AS bizDocType, is_stage AS isStage
  175. FROM mdp_trans_type_map
  176. WHERE tenant_id=0 AND (@sys = '' OR system_code=@sys)
  177. ORDER BY system_code, src_trans_code
  178. LIMIT 500
  179. """,
  180. new { sys = systemCode?.Trim() ?? "" });
  181. }
  182. [HttpPut("trans-type-maps")]
  183. public async Task<object> SaveTransType([FromBody] TransTypeInput input)
  184. {
  185. if (!_user.SuperAdmin && !_user.SysAdmin)
  186. throw Oops.Oh("事务类型全局行只有管理员可以改");
  187. if (string.IsNullOrWhiteSpace(input.SystemCode) || string.IsNullOrWhiteSpace(input.SrcTransCode))
  188. throw Oops.Oh("systemCode 与 srcTransCode 必填");
  189. await _db.Ado.ExecuteCommandAsync(
  190. """
  191. INSERT INTO mdp_trans_type_map
  192. (tenant_id, system_code, src_trans_code, qty_sign, location_role, stage_code, biz_doc_type, is_stage, excluded_by_location)
  193. VALUES (0, @sys, @src, @sgn, @role, @stage, @biz, @isStage, 0)
  194. ON DUPLICATE KEY UPDATE stage_code=VALUES(stage_code), biz_doc_type=VALUES(biz_doc_type), is_stage=VALUES(is_stage)
  195. """,
  196. new
  197. {
  198. sys = input.SystemCode.Trim(),
  199. src = input.SrcTransCode.Trim(),
  200. sgn = input.QtySign,
  201. role = input.LocationRole?.Trim() ?? "",
  202. stage = input.StageCode,
  203. biz = input.BizDocType,
  204. isStage = input.IsStage
  205. });
  206. return new { ok = true };
  207. }
  208. [HttpGet("source-capabilities")]
  209. public Task<List<dynamic>> ListCapabilities([FromQuery] string systemCode)
  210. {
  211. if (string.IsNullOrWhiteSpace(systemCode))
  212. throw Oops.Oh("systemCode 必填");
  213. return _db.Ado.SqlQueryAsync<dynamic>(
  214. """
  215. SELECT semantic_code AS semanticCode, supported, reason, evidence
  216. FROM mdp_source_capability
  217. WHERE source_system=@sys
  218. ORDER BY semantic_code
  219. """,
  220. new { sys = systemCode.Trim() });
  221. }
  222. [HttpPut("source-capabilities")]
  223. public async Task<object> SaveCapability([FromBody] CapabilityInput input)
  224. {
  225. if (string.IsNullOrWhiteSpace(input.SystemCode) || string.IsNullOrWhiteSpace(input.SemanticCode))
  226. throw Oops.Oh("systemCode 与 semanticCode 必填");
  227. await _db.Ado.ExecuteCommandAsync(
  228. """
  229. INSERT INTO mdp_source_capability (source_system, semantic_code, supported, reason, evidence, verified_at)
  230. VALUES (@sys, @code, @sup, @reason, @evidence, NOW())
  231. ON DUPLICATE KEY UPDATE supported=VALUES(supported), reason=VALUES(reason), evidence=VALUES(evidence), verified_at=NOW()
  232. """,
  233. new
  234. {
  235. sys = input.SystemCode.Trim(),
  236. code = input.SemanticCode.Trim(),
  237. sup = input.Supported ? 1 : 0,
  238. reason = input.Reason ?? "",
  239. evidence = input.Evidence
  240. });
  241. return new { ok = true };
  242. }
  243. [HttpGet("inbound-grants")]
  244. public async Task<List<dynamic>> ListGrants([FromQuery] long? tenantId, [FromQuery] string? sourceCode)
  245. {
  246. var tenant = ScopeTenant(tenantId);
  247. var rows = await _db.Ado.SqlQueryAsync<dynamic>(
  248. """
  249. SELECT id, entity_code AS entityCode, source_code AS sourceCode, rate_limit_per_min AS rateLimitPerMin,
  250. status, CONCAT(LEFT(access_key, 4), '…') AS accessKeyMask
  251. FROM mdp_inbound_grant
  252. WHERE tenant_id=@tenant AND (@src = '' OR source_code=@src)
  253. ORDER BY source_code, entity_code
  254. """,
  255. new { tenant, src = sourceCode?.Trim() ?? "" });
  256. return rows;
  257. }
  258. [HttpGet("required-columns")]
  259. public object RequiredColumns([FromQuery] string stdObject)
  260. {
  261. var def = MdpStdObjectCatalog.Find(stdObject);
  262. if (def == null)
  263. throw Oops.Oh($"未知标准对象 {stdObject}");
  264. return new
  265. {
  266. stdObject = def.Code,
  267. targetTable = def.Tables.FirstOrDefault(),
  268. columns = NeutralRequiredColumns.For(def.Code).Select(c => new { name = c.Name, semanticCode = c.SemanticCode })
  269. };
  270. }
  271. [HttpGet("semantic-codes")]
  272. public object SemanticCodes() =>
  273. Admin.NET.Plugin.AiDOP.SmartOps.KpiSemanticDependency.DistinctSemantics();
  274. [HttpPost("inbound-grants")]
  275. public async Task<object> CreateGrant([FromBody] GrantCreateInput input)
  276. {
  277. var tenant = ScopeTenant(input.TenantId);
  278. if (string.IsNullOrWhiteSpace(input.SourceCode) || string.IsNullOrWhiteSpace(input.EntityCode))
  279. throw Oops.Oh("sourceCode 与 entityCode 必填");
  280. var source = await _db.Queryable<MdpSource>()
  281. .Where(s => s.SourceCode == input.SourceCode.Trim())
  282. .FirstAsync() ?? throw Oops.Oh("来源不存在");
  283. if (!string.Equals(source.SourceType, "API_INBOUND", StringComparison.OrdinalIgnoreCase))
  284. throw Oops.Oh("只有对方推送来源可以发授权");
  285. var requestedKey = input.AccessKey?.Trim() ?? "";
  286. var identity = string.IsNullOrEmpty(requestedKey)
  287. ? null
  288. : await _db.Queryable<SysOpenAccess>().FirstAsync(x => x.AccessKey == requestedKey);
  289. var plan = InboundGrantIssuance.Plan(
  290. requestedKey,
  291. tenant,
  292. identity?.BindTenantId ?? 0,
  293. identity != null,
  294. NewToken,
  295. NewToken);
  296. var entityCode = input.EntityCode.Trim();
  297. switch (plan.Kind)
  298. {
  299. case InboundGrantIssuanceKind.RejectUnknownKey:
  300. throw Oops.Oh("访问标识不存在。留空将新建一套凭据,或先在开放接口身份中建立该标识");
  301. case InboundGrantIssuanceKind.RejectCrossTenant:
  302. throw Oops.Oh("该访问标识属于其他租户,不能用于本租户的推送授权");
  303. }
  304. var duplicated = await _db.Queryable<MdpInboundGrant>()
  305. .AnyAsync(x => x.AccessKey == plan.AccessKey && x.EntityCode == entityCode);
  306. if (duplicated)
  307. throw Oops.Oh("该访问标识已经授权过这个业务对象");
  308. long bindUserId = 0;
  309. if (plan.Kind == InboundGrantIssuanceKind.CreateIdentity)
  310. {
  311. bindUserId = await _db.Queryable<SysTenant>()
  312. .Where(t => t.Id == tenant)
  313. .Select(t => t.UserId)
  314. .FirstAsync();
  315. if (bindUserId <= 0)
  316. throw Oops.Oh("租户尚未生成租管用户,无法签发推送身份");
  317. }
  318. var now = DateTime.Now;
  319. try
  320. {
  321. _db.Ado.BeginTran();
  322. if (plan.Kind == InboundGrantIssuanceKind.CreateIdentity)
  323. {
  324. await _db.Insertable(new SysOpenAccess
  325. {
  326. Id = YitIdHelper.NextId(),
  327. AccessKey = plan.AccessKey,
  328. AccessSecret = plan.AccessSecret,
  329. BindTenantId = tenant,
  330. BindUserId = bindUserId,
  331. CreateTime = now,
  332. }).ExecuteCommandAsync();
  333. }
  334. await _db.Insertable(new MdpInboundGrant
  335. {
  336. TenantId = tenant,
  337. AccessKey = plan.AccessKey,
  338. EntityCode = entityCode,
  339. SourceCode = source.SourceCode,
  340. RateLimitPerMin = input.RateLimitPerMin <= 0 ? 60 : input.RateLimitPerMin,
  341. BatchRowLimit = 500,
  342. Status = 1,
  343. CreateTime = now,
  344. UpdateTime = now
  345. }).ExecuteCommandAsync();
  346. _db.Ado.CommitTran();
  347. }
  348. catch
  349. {
  350. _db.Ado.RollbackTran();
  351. throw;
  352. }
  353. if (plan.Kind == InboundGrantIssuanceKind.CreateIdentity)
  354. _cache.Remove(CacheConst.KeyOpenAccess + plan.AccessKey);
  355. // 密钥只在新建时回显一次。复用已有标识时调用方本就持有密钥,不再返回。
  356. return plan.Kind == InboundGrantIssuanceKind.CreateIdentity
  357. ? new { accessKey = plan.AccessKey, accessSecret = plan.AccessSecret, entityCode, secretShownOnce = true }
  358. : (object)new { accessKey = plan.AccessKey, entityCode, secretShownOnce = false };
  359. }
  360. private static string NewToken() =>
  361. Convert.ToHexString(System.Security.Cryptography.RandomNumberGenerator.GetBytes(16)).ToLowerInvariant();
  362. }
  363. public sealed class DomainMapInput
  364. {
  365. public long TenantId { get; set; }
  366. public string SystemCode { get; set; } = "";
  367. public string? SourceCode { get; set; }
  368. public string Domain { get; set; } = "";
  369. public string? Remark { get; set; }
  370. }
  371. public sealed class LocationRoleInput
  372. {
  373. public long TenantId { get; set; }
  374. public List<LocationRoleRow> Rows { get; set; } = [];
  375. }
  376. public sealed class LocationRoleRow
  377. {
  378. public string? Domain { get; set; }
  379. public string Location { get; set; } = "";
  380. public string LocationRole { get; set; } = "";
  381. public string? Remark { get; set; }
  382. }
  383. public sealed class PositionMapInput
  384. {
  385. public long TenantId { get; set; }
  386. public List<PositionMapRow> Rows { get; set; } = [];
  387. }
  388. public sealed class PositionMapRow
  389. {
  390. public string SourceSystem { get; set; } = "";
  391. public string? Domain { get; set; }
  392. public string SrcPositionRaw { get; set; } = "";
  393. public string? SrcPositionField { get; set; }
  394. public string PositionCode { get; set; } = "";
  395. public string? Remark { get; set; }
  396. }
  397. public sealed class TransTypeInput
  398. {
  399. public string SystemCode { get; set; } = "";
  400. public string SrcTransCode { get; set; } = "";
  401. public int QtySign { get; set; }
  402. public string? LocationRole { get; set; }
  403. public string? StageCode { get; set; }
  404. public string? BizDocType { get; set; }
  405. public int IsStage { get; set; } = 1;
  406. }
  407. public sealed class CapabilityInput
  408. {
  409. public string SystemCode { get; set; } = "";
  410. public string SemanticCode { get; set; } = "";
  411. public bool Supported { get; set; }
  412. public string? Reason { get; set; }
  413. public string? Evidence { get; set; }
  414. }
  415. public sealed class GrantCreateInput
  416. {
  417. public long TenantId { get; set; }
  418. public string SourceCode { get; set; } = "";
  419. public string EntityCode { get; set; } = "";
  420. public int RateLimitPerMin { get; set; } = 60;
  421. /// <summary>留空则新建开放接口身份并回显密钥;传入则复用该身份,且必须属于目标租户。</summary>
  422. public string AccessKey { get; set; }
  423. }