MdpSourceConfigService.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
  2. using Microsoft.Extensions.Logging;
  3. namespace Admin.NET.Plugin.AiDOP.DataPlatform;
  4. /// <summary>数据源配置 API(供配置中心 sources 页读取与保存)。</summary>
  5. [ApiDescriptionSettings(Order = 324, Description = "数据中台数据源")]
  6. [Route("api/DataPlatform")]
  7. [NonUnify]
  8. public class MdpSourceConfigService : IDynamicApiController, ITransient
  9. {
  10. private readonly ISqlSugarClient _db;
  11. private readonly UserManager _user;
  12. private readonly MdpSourceScopeFactory _scopes;
  13. private readonly MdpSourcePasswordResolver _passwords;
  14. private readonly ILogger<MdpSourceConfigService> _logger;
  15. public MdpSourceConfigService(
  16. ISqlSugarClient db,
  17. UserManager user,
  18. MdpSourceScopeFactory scopes,
  19. MdpSourcePasswordResolver passwords,
  20. ILogger<MdpSourceConfigService> logger)
  21. {
  22. _db = db;
  23. _user = user;
  24. _scopes = scopes;
  25. _passwords = passwords;
  26. _logger = logger;
  27. }
  28. [DisplayName("数据源列表")]
  29. [HttpGet("sources")]
  30. public async Task<object> GetList([FromQuery] string? keyword, [FromQuery] string? type)
  31. {
  32. var q = _db.Queryable<MdpSource>();
  33. if (!_user.SuperAdmin && !_user.SysAdmin)
  34. q = q.Where(x => x.TenantId == _user.TenantId);
  35. if (!string.IsNullOrWhiteSpace(keyword))
  36. {
  37. var kw = keyword.Trim();
  38. q = q.Where(x => x.SourceCode.Contains(kw) || x.SourceName.Contains(kw));
  39. }
  40. if (!string.IsNullOrWhiteSpace(type))
  41. q = q.Where(x => x.SourceType == type.Trim());
  42. var list = await q.OrderBy(x => x.SourceCode).ToListAsync();
  43. return new
  44. {
  45. list = list.Select(x => new
  46. {
  47. id = x.Id,
  48. sourceCode = x.SourceCode,
  49. systemCode = string.IsNullOrWhiteSpace(x.SystemCode) ? x.SourceCode : x.SystemCode,
  50. connMode = string.IsNullOrWhiteSpace(x.ConnMode) ? "EXTERNAL" : x.ConnMode,
  51. sourceName = x.SourceName,
  52. sourceType = x.SourceType,
  53. status = x.Status,
  54. direction = string.Equals(x.SourceType, "API", StringComparison.OrdinalIgnoreCase) ? "拉取"
  55. : string.Equals(x.SourceType, "FILE_EXCEL", StringComparison.OrdinalIgnoreCase) ? "文件入站" : "入站",
  56. dbType = x.DbType ?? "-",
  57. dbHost = x.DbHost,
  58. dbPort = x.DbPort,
  59. dbName = x.DbName,
  60. dbUser = x.DbUser,
  61. apiBaseUrl = x.ApiBaseUrl,
  62. host = string.Equals(x.SourceType, "API", StringComparison.OrdinalIgnoreCase)
  63. ? (x.ApiBaseUrl ?? "-")
  64. : string.Equals(x.SourceType, "FILE_EXCEL", StringComparison.OrdinalIgnoreCase)
  65. ? "-"
  66. : $"{x.DbHost}/{x.DbName}",
  67. health = string.Equals(x.SourceType, "FILE_EXCEL", StringComparison.OrdinalIgnoreCase)
  68. ? (string.IsNullOrWhiteSpace(x.SourceCode) ? "配置异常" : "可用")
  69. : x.HealthStatus switch
  70. {
  71. 1 => "正常",
  72. 0 => "异常",
  73. _ => "未知"
  74. },
  75. healthType = x.HealthStatus switch
  76. {
  77. 1 => "success",
  78. 0 => "danger",
  79. _ => "info"
  80. },
  81. lastCheck = x.LastHealthCheck?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-",
  82. healthMsg = x.HealthMsg,
  83. dbExtraParams = x.DbExtraParams,
  84. remark = x.Remark,
  85. secretSource = string.Equals(x.SourceType, "DB", StringComparison.OrdinalIgnoreCase)
  86. ? _passwords.Classify(x.SourceCode, x.DbPasswordEnc)
  87. : null
  88. })
  89. };
  90. }
  91. [DisplayName("新建或更新数据源")]
  92. [HttpPost("sources")]
  93. public async Task<object> Save([FromBody] MdpSourceSaveInput input)
  94. {
  95. if (string.IsNullOrWhiteSpace(input.SourceCode) || string.IsNullOrWhiteSpace(input.SourceName) || string.IsNullOrWhiteSpace(input.SourceType))
  96. throw Oops.Oh("sourceCode、sourceName、sourceType 必填");
  97. var code = input.SourceCode.Trim();
  98. var tenantId = _user.TenantId;
  99. var mode = string.IsNullOrWhiteSpace(input.ConnMode) ? "EXTERNAL" : input.ConnMode.Trim().ToUpperInvariant();
  100. if (mode is not ("SELF" or "EXTERNAL"))
  101. throw Oops.Oh("connMode 只能是 SELF 或 EXTERNAL");
  102. if (tenantId == 0)
  103. throw Oops.Oh("系统种子来源只能由迁移脚本维护");
  104. var sourceType = input.SourceType.Trim();
  105. ValidateDbSource(input, sourceType, mode);
  106. var existing = await _db.Queryable<MdpSource>()
  107. .Where(x => x.TenantId == tenantId && x.SourceCode == code)
  108. .FirstAsync();
  109. if (existing == null)
  110. {
  111. await _db.Insertable(new MdpSource
  112. {
  113. TenantId = tenantId,
  114. SourceCode = code,
  115. SourceName = input.SourceName.Trim(),
  116. SourceType = sourceType,
  117. SystemCode = input.SystemCode?.Trim(),
  118. ConnMode = mode,
  119. Status = 1,
  120. DbType = NormalizeDbType(input.DbType),
  121. DbHost = mode == "SELF" ? null : input.DbHost?.Trim(),
  122. DbPort = mode == "SELF" ? null : input.DbPort,
  123. DbName = mode == "SELF" ? null : input.DbName?.Trim(),
  124. DbUser = mode == "SELF" ? null : input.DbUser?.Trim(),
  125. DbPasswordEnc = mode == "SELF" ? null : EncryptPassword(input.DbPassword),
  126. DbExtraParams = mode == "SELF" ? null : MdpSourceScopeFactory.SanitizeStoredExtra(input.DbType, input.DbExtraParams),
  127. ApiBaseUrl = input.ApiBaseUrl?.Trim(),
  128. Remark = input.Remark?.Trim(),
  129. CreateTime = DateTime.Now,
  130. UpdateTime = DateTime.Now
  131. }).ExecuteCommandAsync();
  132. }
  133. else
  134. {
  135. if (existing.TenantId == 0)
  136. throw Oops.Oh("系统种子来源只能由迁移脚本维护");
  137. existing.SourceName = input.SourceName.Trim();
  138. existing.SourceType = sourceType;
  139. existing.SystemCode = input.SystemCode?.Trim();
  140. existing.ConnMode = mode;
  141. existing.DbType = NormalizeDbType(input.DbType);
  142. existing.DbHost = mode == "SELF" ? null : input.DbHost?.Trim();
  143. existing.DbPort = mode == "SELF" ? null : input.DbPort;
  144. existing.DbName = mode == "SELF" ? null : input.DbName?.Trim();
  145. existing.DbUser = mode == "SELF" ? null : input.DbUser?.Trim();
  146. if (mode == "SELF")
  147. existing.DbPasswordEnc = null;
  148. else if (!string.IsNullOrEmpty(input.DbPassword))
  149. existing.DbPasswordEnc = EncryptPassword(input.DbPassword);
  150. existing.DbExtraParams = mode == "SELF" ? null : MdpSourceScopeFactory.SanitizeStoredExtra(input.DbType, input.DbExtraParams);
  151. existing.ApiBaseUrl = input.ApiBaseUrl?.Trim();
  152. existing.Remark = input.Remark?.Trim();
  153. existing.UpdateTime = DateTime.Now;
  154. await _db.Updateable(existing).ExecuteCommandAsync();
  155. }
  156. return new { sourceCode = code };
  157. }
  158. private static void ValidateDbSource(MdpSourceSaveInput input, string sourceType, string mode)
  159. {
  160. if (!string.Equals(sourceType, "DB", StringComparison.OrdinalIgnoreCase))
  161. return;
  162. var dbType = NormalizeDbType(input.DbType);
  163. if (!MdpDbSyncRules.IsSupportedDbType(dbType))
  164. throw Oops.Oh("数据库类型只支持 SQLSERVER 或 MYSQL");
  165. if (mode == "EXTERNAL" && (string.IsNullOrWhiteSpace(input.DbHost) || input.DbPort is null or <= 0
  166. || string.IsNullOrWhiteSpace(input.DbName) || string.IsNullOrWhiteSpace(input.DbUser)))
  167. throw Oops.Oh("外部数据库必须填写主机、端口、库名和账号");
  168. }
  169. private static string? NormalizeDbType(string? dbType) =>
  170. string.IsNullOrWhiteSpace(dbType) ? null : dbType.Trim().ToUpperInvariant();
  171. private static string? EncryptPassword(string? password) =>
  172. string.IsNullOrEmpty(password) ? null : CryptogramUtil.Encrypt(password);
  173. [DisplayName("测试数据源连接")]
  174. [HttpPost("sources/{sourceCode}/test")]
  175. public async Task<object> Test([FromRoute] string sourceCode)
  176. {
  177. var source = await FindOwnSourceAsync(sourceCode);
  178. if (string.Equals(source.SourceType, "DB", StringComparison.OrdinalIgnoreCase))
  179. {
  180. try
  181. {
  182. var started = DateTime.UtcNow;
  183. var scope = await _scopes.GetScopeAsync(source);
  184. await scope.Ado.GetScalarAsync("SELECT 1");
  185. var elapsed = (int)(DateTime.UtcNow - started).TotalMilliseconds;
  186. return new { ok = true, message = "数据库连接成功", dbType = source.DbType, elapsedMs = elapsed };
  187. }
  188. catch (Exception ex)
  189. {
  190. _logger.LogWarning(ex, "数据源 {SourceCode} 测试连接失败,tenant={TenantId}", source.SourceCode, source.TenantId);
  191. var error = MdpDbConnectionError.Describe(ex);
  192. return new { ok = false, code = error.Code, message = error.Message };
  193. }
  194. }
  195. if (string.Equals(source.SourceType, "API", StringComparison.OrdinalIgnoreCase))
  196. {
  197. if (string.IsNullOrWhiteSpace(source.ApiBaseUrl))
  198. return new { ok = false, message = "未填写接口地址" };
  199. try
  200. {
  201. using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
  202. using var resp = await http.GetAsync(source.ApiBaseUrl);
  203. return new { ok = resp.IsSuccessStatusCode, message = $"HTTP {(int)resp.StatusCode}" };
  204. }
  205. catch (Exception)
  206. {
  207. return new { ok = false, message = "接口探测失败" };
  208. }
  209. }
  210. return new { ok = true, message = "对方推送不需要探测对端连接" };
  211. }
  212. [DisplayName("停用数据源前的登记检查")]
  213. [HttpGet("sources/{sourceCode}/bindings")]
  214. public async Task<object> Bindings([FromRoute] string sourceCode)
  215. {
  216. var source = await FindOwnSourceAsync(sourceCode);
  217. var system = string.IsNullOrWhiteSpace(source.SystemCode) ? source.SourceCode : source.SystemCode;
  218. var rows = await _db.Ado.SqlQueryAsync<dynamic>(
  219. """
  220. SELECT tenant_id AS tenantId, std_object AS stdObject
  221. FROM mdp_tenant_std_source
  222. WHERE source_system=@sys
  223. ORDER BY tenant_id, std_object
  224. """,
  225. new { sys = system });
  226. return new { systemCode = system, bindings = rows };
  227. }
  228. [DisplayName("停用数据源")]
  229. [HttpPost("sources/{sourceCode}/disable")]
  230. public async Task<object> Disable([FromRoute] string sourceCode, [FromBody] MdpSourceDisableInput input)
  231. {
  232. var source = await FindOwnSourceAsync(sourceCode);
  233. var system = string.IsNullOrWhiteSpace(source.SystemCode) ? source.SourceCode : source.SystemCode;
  234. var rows = await _db.Ado.SqlQueryAsync<dynamic>(
  235. "SELECT std_object AS stdObject FROM mdp_tenant_std_source WHERE source_system=@sys",
  236. new { sys = system });
  237. if (rows.Count > 0 && input.Confirm != true)
  238. return new { disabled = false, needConfirm = true, bindings = rows };
  239. source.Status = 0;
  240. source.UpdateTime = DateTime.Now;
  241. await _db.Updateable(source).UpdateColumns(x => new { x.Status, x.UpdateTime }).ExecuteCommandAsync();
  242. return new { disabled = true, sourceCode = source.SourceCode };
  243. }
  244. private async Task<MdpSource> FindOwnSourceAsync(string sourceCode)
  245. {
  246. var code = sourceCode.Trim();
  247. var source = await _db.Queryable<MdpSource>().Where(x => x.SourceCode == code).FirstAsync()
  248. ?? throw Oops.Oh($"未找到来源 {code}");
  249. if (!_user.SuperAdmin && !_user.SysAdmin && source.TenantId != _user.TenantId)
  250. throw Oops.Oh("不能操作其他租户的数据源");
  251. return source;
  252. }
  253. }
  254. public sealed class MdpSourceSaveInput
  255. {
  256. public string SourceCode { get; set; } = "";
  257. public string SourceName { get; set; } = "";
  258. public string SourceType { get; set; } = "DB";
  259. public string? SystemCode { get; set; }
  260. public string? ConnMode { get; set; }
  261. public string? DbType { get; set; }
  262. public string? DbHost { get; set; }
  263. public int? DbPort { get; set; }
  264. public string? DbName { get; set; }
  265. public string? DbUser { get; set; }
  266. public string? DbPassword { get; set; }
  267. public string? DbExtraParams { get; set; }
  268. public string? ApiBaseUrl { get; set; }
  269. public string? Remark { get; set; }
  270. }
  271. public sealed class MdpSourceDisableInput
  272. {
  273. public bool? Confirm { get; set; }
  274. }