MdpSyncTaskConfigService.cs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769
  1. using Admin.NET.Plugin.AiDOP.Dto.DataPlatform;
  2. using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
  3. using Admin.NET.Plugin.AiDOP.Order;
  4. namespace Admin.NET.Plugin.AiDOP.DataPlatform;
  5. /// <summary>
  6. /// 数据中台同步配置中心 API。
  7. /// </summary>
  8. [ApiDescriptionSettings(Order = 323, Description = "数据中台同步配置中心")]
  9. [Route("api/DataPlatform")]
  10. [NonUnify]
  11. public class MdpSyncTaskConfigService : IDynamicApiController, ITransient
  12. {
  13. private static readonly HashSet<string> ProtectedTaskCodes = new(StringComparer.OrdinalIgnoreCase)
  14. {
  15. "S1_MDP_SYNC_TRANSFORM",
  16. "S2_MDP_SYNC_TRANSFORM",
  17. "S3_MDP_SYNC_TRANSFORM",
  18. "S4_MDP_SYNC_TRANSFORM",
  19. "S5_MDP_SYNC_TRANSFORM",
  20. "S5_PURCHASE_RECEIPT_INBOUND",
  21. "S6_MDP_SYNC_TRANSFORM",
  22. "S6_IPQC_INSPECTION_INBOUND",
  23. "S7_MDP_SYNC_TRANSFORM",
  24. "S7_PRODUCTION_RECEIPT_INBOUND"
  25. };
  26. private readonly ISqlSugarClient _db;
  27. private readonly UserManager _userManager;
  28. private readonly MdpDbSyncScheduleBinder _scheduleBinder;
  29. private readonly MdpDbEntityOperationService _dbOperations;
  30. private readonly MdpDbSyncNeutralProjection _neutralProjection;
  31. private readonly MdpDbSyncRebuildTrigger _rebuildTrigger;
  32. public MdpSyncTaskConfigService(
  33. ISqlSugarClient db,
  34. UserManager userManager,
  35. MdpDbSyncScheduleBinder scheduleBinder,
  36. MdpDbEntityOperationService dbOperations,
  37. MdpDbSyncNeutralProjection neutralProjection,
  38. MdpDbSyncRebuildTrigger rebuildTrigger)
  39. {
  40. _db = db;
  41. _userManager = userManager;
  42. _scheduleBinder = scheduleBinder;
  43. _dbOperations = dbOperations;
  44. _neutralProjection = neutralProjection;
  45. _rebuildTrigger = rebuildTrigger;
  46. }
  47. [DisplayName("同步任务列表")]
  48. [HttpGet("sync-tasks")]
  49. public async Task<object> GetList([FromQuery] MdpSyncTaskListQuery input)
  50. {
  51. var tenantId = _userManager.TenantId;
  52. var page = input.Page <= 0 ? 1 : input.Page;
  53. var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
  54. var offset = (page - 1) * pageSize;
  55. var where = new List<string> { "(t.tenant_id = @TenantId OR t.tenant_id = 0)", "t.status = 1" };
  56. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  57. if (!string.IsNullOrWhiteSpace(input.Keyword))
  58. {
  59. where.Add("(t.task_code LIKE @Keyword OR t.task_name LIKE @Keyword OR t.business_domain_name LIKE @Keyword)");
  60. pars.Add(new SugarParameter("@Keyword", $"%{input.Keyword.Trim()}%"));
  61. }
  62. if (!string.IsNullOrWhiteSpace(input.BusinessDomainCode))
  63. {
  64. where.Add("t.business_domain_code = @BusinessDomainCode");
  65. pars.Add(new SugarParameter("@BusinessDomainCode", input.BusinessDomainCode.Trim()));
  66. }
  67. if (input.Status.HasValue)
  68. {
  69. where[1] = "t.status = @Status";
  70. pars.Add(new SugarParameter("@Status", input.Status.Value));
  71. }
  72. var whereSql = string.Join(" AND ", where);
  73. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM mdp_sync_task t WHERE {whereSql}", pars);
  74. var list = await _db.Ado.SqlQueryAsync<MdpSyncTaskListRow>(
  75. $"""
  76. SELECT t.id AS Id, t.tenant_id AS TenantId, t.task_code AS TaskCode, t.task_name AS TaskName,
  77. t.task_type AS TaskType, t.business_domain_code AS BusinessDomainCode,
  78. t.business_domain_name AS BusinessDomainName, t.consumer_modules AS ConsumerModules,
  79. t.source_system_code AS SourceSystemCode, t.service_key AS ServiceKey,
  80. t.job_code AS JobCode, t.schedule_job_id AS ScheduleJobId, t.status AS Status,
  81. t.config_version AS ConfigVersion, t.description AS Description,
  82. sch.schedule_mode AS ScheduleMode,
  83. IFNULL(sch.auto_enabled, 1) AS AutoEnabled,
  84. IFNULL(sch.manual_enabled, 1) AS ManualEnabled,
  85. run.status AS LastRunStatus, run.start_time AS LastRunTime
  86. FROM mdp_sync_task t
  87. LEFT JOIN mdp_sync_task_schedule sch
  88. ON sch.tenant_id = t.tenant_id AND sch.task_code = t.task_code
  89. LEFT JOIN (
  90. SELECT job_code, MAX(start_time) AS last_run_time
  91. FROM mdp_transform_run_log
  92. WHERE {MdpMonitorService.BuildMdpRunLogTenantWhere(tenantId)}
  93. GROUP BY job_code
  94. ) latest ON latest.job_code = IFNULL(t.job_code, t.task_code)
  95. LEFT JOIN mdp_transform_run_log run
  96. ON run.job_code = latest.job_code AND run.start_time = latest.last_run_time
  97. WHERE {whereSql}
  98. ORDER BY t.task_code
  99. LIMIT {pageSize} OFFSET {offset}
  100. """,
  101. pars);
  102. return new { total, page, pageSize, list };
  103. }
  104. [DisplayName("同步任务详情")]
  105. [HttpGet("sync-tasks/{id:long}")]
  106. public async Task<object> GetDetail(long id)
  107. {
  108. var tenantId = _userManager.TenantId;
  109. var row = await _db.Ado.SqlQuerySingleAsync<MdpSyncTaskListRow>(
  110. $"""
  111. SELECT t.id AS Id, t.tenant_id AS TenantId, t.task_code AS TaskCode, t.task_name AS TaskName,
  112. t.task_type AS TaskType, t.business_domain_code AS BusinessDomainCode,
  113. t.business_domain_name AS BusinessDomainName, t.consumer_modules AS ConsumerModules,
  114. t.source_system_code AS SourceSystemCode, t.service_key AS ServiceKey,
  115. t.job_code AS JobCode, t.schedule_job_id AS ScheduleJobId, t.status AS Status,
  116. t.config_version AS ConfigVersion, t.description AS Description,
  117. sch.schedule_mode AS ScheduleMode,
  118. IFNULL(sch.auto_enabled, 1) AS AutoEnabled,
  119. IFNULL(sch.manual_enabled, 1) AS ManualEnabled,
  120. run.status AS LastRunStatus, run.start_time AS LastRunTime
  121. FROM mdp_sync_task t
  122. LEFT JOIN mdp_sync_task_schedule sch
  123. ON sch.tenant_id = t.tenant_id AND sch.task_code = t.task_code
  124. LEFT JOIN (
  125. SELECT job_code, MAX(start_time) AS last_run_time
  126. FROM mdp_transform_run_log
  127. WHERE {MdpMonitorService.BuildMdpRunLogTenantWhere(tenantId)}
  128. GROUP BY job_code
  129. ) latest ON latest.job_code = IFNULL(t.job_code, t.task_code)
  130. LEFT JOIN mdp_transform_run_log run
  131. ON run.job_code = latest.job_code AND run.start_time = latest.last_run_time
  132. WHERE t.id = @Id AND (t.tenant_id = @TenantId OR t.tenant_id = 0)
  133. LIMIT 1
  134. """,
  135. new SugarParameter("@Id", id),
  136. new SugarParameter("@TenantId", tenantId));
  137. if (row == null)
  138. throw Oops.Oh("同步任务不存在");
  139. return row;
  140. }
  141. [DisplayName("立即执行数据库同步任务")]
  142. [HttpPost("sync-tasks/{taskCode}/run")]
  143. public async Task<object> RunDbSyncTask(
  144. string taskCode,
  145. [FromBody] MdpSyncTaskRunInput input,
  146. CancellationToken cancellationToken)
  147. {
  148. var tenantId = _userManager.TenantId;
  149. if (tenantId <= 0)
  150. throw Oops.Oh("无法解析当前租户");
  151. if (string.IsNullOrWhiteSpace(input.Reason))
  152. throw Oops.Oh("请填写执行原因");
  153. var task = await EnsureTaskExistsAsync(taskCode, tenantId);
  154. if (!string.Equals(task.TaskType, "DB_SYNC", StringComparison.OrdinalIgnoreCase))
  155. throw Oops.Oh("只有方式甲数据库同步任务支持该操作");
  156. var mode = (input.Mode ?? "INCR").Trim().ToUpperInvariant();
  157. if (mode is not ("FULL" or "INCR" or "ROLLING"))
  158. throw Oops.Oh("执行模式只能是 FULL、INCR 或 ROLLING");
  159. var jobCode = string.IsNullOrWhiteSpace(task.JobCode) ? task.TaskCode : task.JobCode;
  160. var entities = await _db.Queryable<MdpEntity>()
  161. .Where(x => x.Status == 1
  162. && (x.TenantId == tenantId || x.TenantId == 0)
  163. && (x.JobId == task.TaskCode || x.JobId == jobCode))
  164. .OrderByDescending(x => x.TenantId)
  165. .OrderBy(x => x.EntityCode)
  166. .ToListAsync(cancellationToken);
  167. if (entities.Count == 0)
  168. throw Oops.Oh("任务没有已启用的同步实体");
  169. var results = new List<object>();
  170. foreach (var entity in entities
  171. .GroupBy(x => x.EntityCode, StringComparer.OrdinalIgnoreCase)
  172. .Select(x => x.First()))
  173. {
  174. cancellationToken.ThrowIfCancellationRequested();
  175. var result = await _dbOperations.RunEntityAsync(
  176. entity.Id,
  177. mode,
  178. input.Reason.Trim(),
  179. "MANUAL",
  180. cancellationToken,
  181. tenantId);
  182. results.Add(result);
  183. }
  184. await _neutralProjection.ProjectAsync(entities, tenantId, cancellationToken);
  185. var modules = await _rebuildTrigger.EnqueueForTaskAsync(
  186. task,
  187. tenantId,
  188. input.FactoryId > 0 ? input.FactoryId : 1,
  189. "MANUAL",
  190. cancellationToken,
  191. _userManager.UserId);
  192. return new { taskCode = task.TaskCode, entityCount = results.Count, modules, results };
  193. }
  194. [DisplayName("新增同步任务")]
  195. [HttpPost("sync-tasks")]
  196. public async Task<object> Create([FromBody] MdpSyncTaskUpsertInput input)
  197. {
  198. ValidateUpsert(input);
  199. var tenantId = _userManager.TenantId;
  200. var exists = await _db.Queryable<MdpSyncTask>()
  201. .Where(u => u.TenantId == tenantId && u.TaskCode == input.TaskCode.Trim())
  202. .AnyAsync();
  203. if (exists)
  204. throw Oops.Oh("任务编码已存在");
  205. var entity = MapUpsert(input, tenantId);
  206. entity.ConfigVersion = 1;
  207. entity.CreateTime = DateTime.Now;
  208. entity.UpdateTime = DateTime.Now;
  209. var id = await _db.Insertable(entity).ExecuteReturnBigIdentityAsync();
  210. await EnsureDefaultScheduleAsync(tenantId, entity.TaskCode, entity.ScheduleJobId);
  211. return new { id };
  212. }
  213. [DisplayName("更新同步任务")]
  214. [HttpPut("sync-tasks/{id:long}")]
  215. public async Task<object> Update(long id, [FromBody] MdpSyncTaskUpsertInput input)
  216. {
  217. ValidateUpsert(input);
  218. var tenantId = _userManager.TenantId;
  219. var entity = await FindEditableTaskAsync(id, tenantId);
  220. entity.TaskName = input.TaskName.Trim();
  221. entity.TaskType = string.IsNullOrWhiteSpace(input.TaskType) ? "SERVICE_SYNC" : input.TaskType.Trim();
  222. entity.BusinessDomainCode = input.BusinessDomainCode?.Trim();
  223. entity.BusinessDomainName = input.BusinessDomainName?.Trim();
  224. entity.ConsumerModules = input.ConsumerModules?.Trim();
  225. entity.SourceSystemCode = input.SourceSystemCode?.Trim();
  226. entity.ServiceKey = input.ServiceKey?.Trim();
  227. entity.JobCode = string.IsNullOrWhiteSpace(input.JobCode) ? input.TaskCode.Trim() : input.JobCode.Trim();
  228. entity.ScheduleJobId = input.ScheduleJobId?.Trim();
  229. entity.Status = input.Status <= 0 ? 0 : 1;
  230. entity.OwnerRole = input.OwnerRole?.Trim();
  231. entity.Description = input.Description?.Trim();
  232. entity.ConfigVersion += 1;
  233. entity.UpdateTime = DateTime.Now;
  234. if (!ProtectedTaskCodes.Contains(entity.TaskCode))
  235. entity.TaskCode = input.TaskCode.Trim();
  236. await _db.Updateable(entity).ExecuteCommandAsync();
  237. await SyncScheduleJobIdAsync(entity.TenantId, entity.TaskCode, entity.ScheduleJobId);
  238. return new { id = entity.Id };
  239. }
  240. [DisplayName("删除同步任务")]
  241. [HttpDelete("sync-tasks/{id:long}")]
  242. public async Task<object> Delete(long id)
  243. {
  244. var tenantId = _userManager.TenantId;
  245. var entity = await FindEditableTaskAsync(id, tenantId);
  246. if (ProtectedTaskCodes.Contains(entity.TaskCode) && entity.TenantId == 0)
  247. throw Oops.Oh("内置 MDP 同步任务不允许删除");
  248. entity.Status = 0;
  249. entity.UpdateTime = DateTime.Now;
  250. await _db.Updateable(entity).UpdateColumns(u => new { u.Status, u.UpdateTime }).ExecuteCommandAsync();
  251. return new { id };
  252. }
  253. [DisplayName("同步任务步骤列表")]
  254. [HttpGet("sync-tasks/{taskCode}/steps")]
  255. public async Task<object> GetSteps(string taskCode)
  256. {
  257. var tenantId = _userManager.TenantId;
  258. await EnsureTaskExistsAsync(taskCode, tenantId);
  259. var steps = await _db.Queryable<MdpSyncTaskStep>()
  260. .Where(u => u.TaskCode == taskCode && (u.TenantId == tenantId || u.TenantId == 0))
  261. .OrderBy(u => u.SortOrder)
  262. .ToListAsync();
  263. return steps.Select(MapStepRow).ToList();
  264. }
  265. [DisplayName("保存同步任务步骤")]
  266. [HttpPut("sync-tasks/{taskCode}/steps")]
  267. public async Task<object> SaveSteps(string taskCode, [FromBody] List<MdpSyncTaskStepRow> input)
  268. {
  269. var tenantId = _userManager.TenantId;
  270. var task = await EnsureTaskExistsAsync(taskCode, tenantId);
  271. var rows = input ?? new List<MdpSyncTaskStepRow>();
  272. var now = DateTime.Now;
  273. foreach (var row in rows)
  274. {
  275. if (string.IsNullOrWhiteSpace(row.StepCode) || string.IsNullOrWhiteSpace(row.StepName))
  276. throw Oops.Oh("步骤编码和名称不能为空");
  277. var stage = string.IsNullOrWhiteSpace(row.StageType) ? row.StepCode.Trim() : row.StageType.Trim();
  278. if (row.Enabled && Admin.NET.Plugin.AiDOP.DataPlatform.FileImport.MdpExcelValidation.IsPullStage(stage, row.StepCode))
  279. {
  280. if (!string.IsNullOrWhiteSpace(task.SourceSystemCode))
  281. {
  282. var src = await _db.Queryable<MdpSource>().FirstAsync(x => x.SourceCode == task.SourceSystemCode);
  283. if (src != null && string.Equals(src.SourceType, "FILE_EXCEL", StringComparison.OrdinalIgnoreCase))
  284. throw Oops.Oh("FILE_EXCEL 实体不得登记 Pull/Extract 步骤,请使用文件导入接口");
  285. }
  286. }
  287. var entity = await _db.Queryable<MdpSyncTaskStep>()
  288. .Where(u => u.TaskCode == taskCode && u.StepCode == row.StepCode && (u.TenantId == tenantId || u.TenantId == 0))
  289. .FirstAsync();
  290. if (entity == null)
  291. {
  292. entity = new MdpSyncTaskStep
  293. {
  294. TenantId = task.TenantId == 0 ? 0 : tenantId,
  295. TaskCode = taskCode,
  296. StepCode = row.StepCode.Trim(),
  297. StepName = row.StepName.Trim(),
  298. StageType = string.IsNullOrWhiteSpace(row.StageType) ? row.StepCode.Trim() : row.StageType.Trim(),
  299. ServiceMethodKey = row.ServiceMethodKey?.Trim(),
  300. Enabled = row.Enabled ? 1 : 0,
  301. SortOrder = row.SortOrder,
  302. Description = row.Description?.Trim(),
  303. CreateTime = now,
  304. UpdateTime = now
  305. };
  306. await _db.Insertable(entity).ExecuteCommandAsync();
  307. }
  308. else
  309. {
  310. entity.StepName = row.StepName.Trim();
  311. entity.StageType = string.IsNullOrWhiteSpace(row.StageType) ? row.StepCode.Trim() : row.StageType.Trim();
  312. entity.ServiceMethodKey = row.ServiceMethodKey?.Trim();
  313. entity.Enabled = row.Enabled ? 1 : 0;
  314. entity.SortOrder = row.SortOrder;
  315. entity.Description = row.Description?.Trim();
  316. entity.UpdateTime = now;
  317. await _db.Updateable(entity).ExecuteCommandAsync();
  318. }
  319. }
  320. task.ConfigVersion += 1;
  321. task.UpdateTime = now;
  322. await _db.Updateable(task).UpdateColumns(u => new { u.ConfigVersion, u.UpdateTime }).ExecuteCommandAsync();
  323. return new { count = rows.Count };
  324. }
  325. [DisplayName("同步任务调度配置")]
  326. [HttpGet("sync-tasks/{taskCode}/schedule")]
  327. public async Task<object> GetSchedule(string taskCode)
  328. {
  329. var tenantId = _userManager.TenantId;
  330. await EnsureTaskExistsAsync(taskCode, tenantId);
  331. var schedule = await FindScheduleAsync(taskCode, tenantId);
  332. if (schedule == null)
  333. return new MdpSyncTaskScheduleRow { TaskCode = taskCode, ScheduleMode = "CRON" };
  334. return MapScheduleRow(schedule);
  335. }
  336. [DisplayName("保存同步任务调度配置")]
  337. [HttpPut("sync-tasks/{taskCode}/schedule")]
  338. public async Task<object> SaveSchedule(string taskCode, [FromBody] MdpSyncTaskScheduleRow input)
  339. {
  340. var tenantId = _userManager.TenantId;
  341. var task = await EnsureTaskExistsAsync(taskCode, tenantId);
  342. var schedule = await FindScheduleAsync(taskCode, tenantId);
  343. var now = DateTime.Now;
  344. if (schedule == null)
  345. {
  346. schedule = new MdpSyncTaskSchedule
  347. {
  348. TenantId = task.TenantId == 0 ? 0 : tenantId,
  349. TaskCode = taskCode,
  350. CreateTime = now,
  351. UpdateTime = now
  352. };
  353. }
  354. schedule.ScheduleJobId = input.ScheduleJobId?.Trim() ?? task.ScheduleJobId;
  355. schedule.ScheduleMode = string.IsNullOrWhiteSpace(input.ScheduleMode) ? "CRON" : input.ScheduleMode.Trim();
  356. schedule.CronExpr = input.CronExpr?.Trim();
  357. schedule.CronDesc = input.CronDesc?.Trim();
  358. schedule.Timezone = string.IsNullOrWhiteSpace(input.Timezone) ? "Asia/Shanghai" : input.Timezone.Trim();
  359. schedule.AutoEnabled = input.AutoEnabled ? 1 : 0;
  360. schedule.ManualEnabled = input.ManualEnabled ? 1 : 0;
  361. schedule.RetryEnabled = input.RetryEnabled ? 1 : 0;
  362. schedule.MaxRetryCount = input.MaxRetryCount <= 0 ? 3 : input.MaxRetryCount;
  363. schedule.RetryIntervalSeconds = input.RetryIntervalSeconds <= 0 ? 300 : input.RetryIntervalSeconds;
  364. schedule.TimeoutSeconds = input.TimeoutSeconds <= 0 ? 3600 : input.TimeoutSeconds;
  365. schedule.MisfirePolicy = input.MisfirePolicy?.Trim();
  366. schedule.SyncWindowType = string.IsNullOrWhiteSpace(input.SyncWindowType) ? "FULL" : input.SyncWindowType.Trim();
  367. schedule.SyncWindowValue = input.SyncWindowValue?.Trim();
  368. schedule.LastScheduleTime = input.LastScheduleTime;
  369. schedule.NextScheduleTime = input.NextScheduleTime;
  370. schedule.AdminJobConfigJson = input.AdminJobConfigJson;
  371. schedule.Description = input.Description?.Trim();
  372. schedule.UpdateTime = now;
  373. if (schedule.Id <= 0)
  374. await _db.Insertable(schedule).ExecuteCommandAsync();
  375. else
  376. await _db.Updateable(schedule).ExecuteCommandAsync();
  377. task.ScheduleJobId = schedule.ScheduleJobId;
  378. task.ConfigVersion += 1;
  379. task.UpdateTime = now;
  380. if (string.Equals(task.TaskType, "DB_SYNC", StringComparison.OrdinalIgnoreCase))
  381. {
  382. var jobId = await _scheduleBinder.BindAsync(task, schedule, tenantId);
  383. schedule.ScheduleJobId = jobId;
  384. task.ScheduleJobId = jobId;
  385. await _db.Updateable(schedule).UpdateColumns(u => new { u.ScheduleJobId, u.UpdateTime }).ExecuteCommandAsync();
  386. }
  387. await _db.Updateable(task).UpdateColumns(u => new { u.ScheduleJobId, u.ConfigVersion, u.UpdateTime }).ExecuteCommandAsync();
  388. return MapScheduleRow(schedule);
  389. }
  390. [DisplayName("同步 Admin.NET 调度快照")]
  391. [HttpPost("sync-tasks/{taskCode}/schedule/sync-admin-job")]
  392. public async Task<object> SyncAdminJob(string taskCode)
  393. {
  394. var tenantId = _userManager.TenantId;
  395. var task = await EnsureTaskExistsAsync(taskCode, tenantId);
  396. var schedule = await FindScheduleAsync(taskCode, tenantId) ?? new MdpSyncTaskSchedule
  397. {
  398. TenantId = task.TenantId == 0 ? 0 : tenantId,
  399. TaskCode = taskCode,
  400. ScheduleJobId = task.ScheduleJobId,
  401. ScheduleMode = "CRON",
  402. CreateTime = DateTime.Now,
  403. UpdateTime = DateTime.Now
  404. };
  405. if (string.Equals(task.TaskType, "DB_SYNC", StringComparison.OrdinalIgnoreCase))
  406. {
  407. var jobId = await _scheduleBinder.BindAsync(task, schedule, tenantId);
  408. schedule.ScheduleJobId = jobId;
  409. task.ScheduleJobId = jobId;
  410. await _db.Updateable(task).UpdateColumns(u => new { u.ScheduleJobId, u.UpdateTime }).ExecuteCommandAsync();
  411. }
  412. schedule.AdminJobConfigJson = System.Text.Json.JsonSerializer.Serialize(new
  413. {
  414. scheduleJobId = schedule.ScheduleJobId ?? task.ScheduleJobId,
  415. jobType = "Admin.NET.Plugin.AiDOP.Job.MdpConfiguredDbSyncJob",
  416. message = string.Equals(task.TaskType, "DB_SYNC", StringComparison.OrdinalIgnoreCase)
  417. ? "已同步到 Admin.NET 调度作业"
  418. : "非数据库同步任务只保存调度快照"
  419. });
  420. schedule.UpdateTime = DateTime.Now;
  421. if (schedule.Id <= 0)
  422. await _db.Insertable(schedule).ExecuteCommandAsync();
  423. else
  424. await _db.Updateable(schedule).UpdateColumns(u => new { u.AdminJobConfigJson, u.UpdateTime }).ExecuteCommandAsync();
  425. return new
  426. {
  427. taskCode,
  428. scheduleJobId = schedule.ScheduleJobId ?? task.ScheduleJobId,
  429. adminJobConfigJson = schedule.AdminJobConfigJson
  430. };
  431. }
  432. private async Task<MdpSyncTask> EnsureTaskExistsAsync(string taskCode, long tenantId)
  433. {
  434. if (string.IsNullOrWhiteSpace(taskCode))
  435. throw Oops.Oh("任务编码不能为空");
  436. var task = await _db.Queryable<MdpSyncTask>()
  437. .Where(u => u.TaskCode == taskCode && (u.TenantId == tenantId || u.TenantId == 0) && u.Status == 1)
  438. .OrderBy(u => u.TenantId, OrderByType.Desc)
  439. .FirstAsync();
  440. if (task == null)
  441. throw Oops.Oh("同步任务不存在");
  442. return task;
  443. }
  444. private async Task<MdpSyncTask> FindEditableTaskAsync(long id, long tenantId)
  445. {
  446. var entity = await _db.Queryable<MdpSyncTask>()
  447. .Where(u => u.Id == id && (u.TenantId == tenantId || u.TenantId == 0))
  448. .FirstAsync();
  449. if (entity == null)
  450. throw Oops.Oh("同步任务不存在");
  451. return entity;
  452. }
  453. private async Task<MdpSyncTaskSchedule?> FindScheduleAsync(string taskCode, long tenantId) =>
  454. await _db.Queryable<MdpSyncTaskSchedule>()
  455. .Where(u => u.TaskCode == taskCode && (u.TenantId == tenantId || u.TenantId == 0))
  456. .OrderBy(u => u.TenantId, OrderByType.Desc)
  457. .FirstAsync();
  458. private async Task EnsureDefaultScheduleAsync(long tenantId, string taskCode, string? scheduleJobId)
  459. {
  460. var exists = await _db.Queryable<MdpSyncTaskSchedule>()
  461. .Where(u => u.TaskCode == taskCode && (u.TenantId == tenantId || u.TenantId == 0))
  462. .AnyAsync();
  463. if (exists)
  464. return;
  465. await _db.Insertable(new MdpSyncTaskSchedule
  466. {
  467. TenantId = tenantId,
  468. TaskCode = taskCode,
  469. ScheduleJobId = scheduleJobId,
  470. ScheduleMode = "CRON",
  471. CronDesc = "按 Admin.NET 任务调度执行",
  472. AutoEnabled = 1,
  473. ManualEnabled = 1,
  474. RetryEnabled = 1,
  475. MaxRetryCount = 3,
  476. RetryIntervalSeconds = 300,
  477. TimeoutSeconds = 3600,
  478. SyncWindowType = "FULL",
  479. CreateTime = DateTime.Now,
  480. UpdateTime = DateTime.Now
  481. }).ExecuteCommandAsync();
  482. }
  483. private async Task SyncScheduleJobIdAsync(long tenantId, string taskCode, string? scheduleJobId)
  484. {
  485. var schedule = await FindScheduleAsync(taskCode, tenantId);
  486. if (schedule == null)
  487. {
  488. await EnsureDefaultScheduleAsync(tenantId, taskCode, scheduleJobId);
  489. return;
  490. }
  491. schedule.ScheduleJobId = scheduleJobId;
  492. schedule.UpdateTime = DateTime.Now;
  493. await _db.Updateable(schedule).UpdateColumns(u => new { u.ScheduleJobId, u.UpdateTime }).ExecuteCommandAsync();
  494. }
  495. private static void ValidateUpsert(MdpSyncTaskUpsertInput input)
  496. {
  497. if (string.IsNullOrWhiteSpace(input.TaskCode))
  498. throw Oops.Oh("任务编码不能为空");
  499. if (string.IsNullOrWhiteSpace(input.TaskName))
  500. throw Oops.Oh("任务名称不能为空");
  501. }
  502. private static MdpSyncTask MapUpsert(MdpSyncTaskUpsertInput input, long tenantId) =>
  503. new()
  504. {
  505. TenantId = tenantId,
  506. TaskCode = input.TaskCode.Trim(),
  507. TaskName = input.TaskName.Trim(),
  508. TaskType = string.IsNullOrWhiteSpace(input.TaskType) ? "SERVICE_SYNC" : input.TaskType.Trim(),
  509. BusinessDomainCode = input.BusinessDomainCode?.Trim(),
  510. BusinessDomainName = input.BusinessDomainName?.Trim(),
  511. ConsumerModules = input.ConsumerModules?.Trim(),
  512. SourceSystemCode = input.SourceSystemCode?.Trim(),
  513. ServiceKey = input.ServiceKey?.Trim(),
  514. JobCode = string.IsNullOrWhiteSpace(input.JobCode) ? input.TaskCode.Trim() : input.JobCode.Trim(),
  515. ScheduleJobId = input.ScheduleJobId?.Trim(),
  516. Status = input.Status <= 0 ? 0 : 1,
  517. OwnerRole = input.OwnerRole?.Trim(),
  518. Description = input.Description?.Trim()
  519. };
  520. private static MdpSyncTaskStepRow MapStepRow(MdpSyncTaskStep step) =>
  521. new()
  522. {
  523. Id = step.Id,
  524. StepCode = step.StepCode,
  525. StepName = step.StepName,
  526. StageType = step.StageType,
  527. ServiceMethodKey = step.ServiceMethodKey,
  528. Enabled = step.Enabled != 0,
  529. SortOrder = step.SortOrder,
  530. Description = step.Description
  531. };
  532. private static MdpSyncTaskScheduleRow MapScheduleRow(MdpSyncTaskSchedule schedule) =>
  533. new()
  534. {
  535. TaskCode = schedule.TaskCode,
  536. ScheduleJobId = schedule.ScheduleJobId,
  537. ScheduleMode = schedule.ScheduleMode,
  538. CronExpr = schedule.CronExpr,
  539. CronDesc = schedule.CronDesc,
  540. Timezone = schedule.Timezone,
  541. AutoEnabled = schedule.AutoEnabled != 0,
  542. ManualEnabled = schedule.ManualEnabled != 0,
  543. RetryEnabled = schedule.RetryEnabled != 0,
  544. MaxRetryCount = schedule.MaxRetryCount,
  545. RetryIntervalSeconds = schedule.RetryIntervalSeconds,
  546. TimeoutSeconds = schedule.TimeoutSeconds,
  547. MisfirePolicy = schedule.MisfirePolicy,
  548. SyncWindowType = schedule.SyncWindowType,
  549. SyncWindowValue = schedule.SyncWindowValue,
  550. LastScheduleTime = schedule.LastScheduleTime,
  551. NextScheduleTime = schedule.NextScheduleTime,
  552. AdminJobConfigJson = schedule.AdminJobConfigJson,
  553. Description = schedule.Description
  554. };
  555. [DisplayName("任务参数列表")]
  556. [HttpGet("sync-tasks/{taskCode}/params")]
  557. public async Task<object> GetParams(string taskCode)
  558. {
  559. var tenantId = _userManager.TenantId;
  560. var list = await _db.Queryable<MdpSyncTaskParam>()
  561. .Where(u => u.TaskCode == taskCode && (u.TenantId == tenantId || u.TenantId == 0))
  562. .OrderBy(u => u.SortOrder)
  563. .ToListAsync();
  564. return new
  565. {
  566. list = list.Select(p => new
  567. {
  568. p.Id,
  569. p.TaskCode,
  570. p.ScopeType,
  571. p.ScopeCode,
  572. p.ParamKey,
  573. p.ParamName,
  574. p.ParamType,
  575. p.ParamValue,
  576. p.DefaultValue,
  577. required = p.Required != 0,
  578. editable = p.Editable != 0,
  579. p.SortOrder,
  580. p.Description
  581. })
  582. };
  583. }
  584. [DisplayName("保存任务参数")]
  585. [HttpPut("sync-tasks/{taskCode}/params")]
  586. public async Task<object> SaveParams(string taskCode, [FromBody] List<MdpSyncTaskParamSaveItem> items)
  587. {
  588. var tenantId = _userManager.TenantId;
  589. items ??= [];
  590. await _db.Deleteable<MdpSyncTaskParam>()
  591. .Where(u => u.TaskCode == taskCode && u.TenantId == tenantId)
  592. .ExecuteCommandAsync();
  593. var now = DateTime.Now;
  594. var rows = items.Where(x => !string.IsNullOrWhiteSpace(x.ParamKey)).Select((x, i) => new MdpSyncTaskParam
  595. {
  596. TenantId = tenantId,
  597. TaskCode = taskCode,
  598. ScopeType = string.IsNullOrWhiteSpace(x.ScopeType) ? "TASK" : x.ScopeType.Trim(),
  599. ScopeCode = x.ScopeCode?.Trim() ?? "",
  600. ParamKey = x.ParamKey.Trim(),
  601. ParamName = string.IsNullOrWhiteSpace(x.ParamName) ? x.ParamKey.Trim() : x.ParamName.Trim(),
  602. ParamType = string.IsNullOrWhiteSpace(x.ParamType) ? "STRING" : x.ParamType.Trim(),
  603. ParamValue = x.ParamValue,
  604. DefaultValue = x.DefaultValue,
  605. Required = x.Required ? (byte)1 : (byte)0,
  606. Editable = x.Editable ? (byte)1 : (byte)0,
  607. SortOrder = x.SortOrder > 0 ? x.SortOrder : i + 1,
  608. Description = x.Description,
  609. CreateTime = now,
  610. UpdateTime = now
  611. }).ToList();
  612. if (rows.Count > 0)
  613. await _db.Insertable(rows).ExecuteCommandAsync();
  614. return new { ok = true, count = rows.Count };
  615. }
  616. [DisplayName("任务公式列表")]
  617. [HttpGet("sync-tasks/{taskCode}/formulas")]
  618. public async Task<object> GetFormulas(string taskCode)
  619. {
  620. var tenantId = _userManager.TenantId;
  621. var list = await _db.Queryable<MdpSyncTaskFormula>()
  622. .Where(u => u.TaskCode == taskCode && (u.TenantId == tenantId || u.TenantId == 0))
  623. .OrderBy(u => u.SortOrder)
  624. .ToListAsync();
  625. return new
  626. {
  627. list = list.Select(f => new
  628. {
  629. f.Id,
  630. f.TaskCode,
  631. f.StepCode,
  632. f.FormulaCode,
  633. f.FormulaName,
  634. f.MetricCode,
  635. f.FormulaExpr,
  636. f.FormulaPreview,
  637. f.Direction,
  638. f.YellowThreshold,
  639. f.RedThreshold,
  640. f.VersionNo,
  641. enabled = f.IsEnabled != 0,
  642. f.SortOrder,
  643. f.Description
  644. })
  645. };
  646. }
  647. [DisplayName("保存任务公式")]
  648. [HttpPut("sync-tasks/{taskCode}/formulas")]
  649. public async Task<object> SaveFormulas(string taskCode, [FromBody] List<MdpSyncTaskFormulaSaveItem> items)
  650. {
  651. var tenantId = _userManager.TenantId;
  652. items ??= [];
  653. await _db.Deleteable<MdpSyncTaskFormula>()
  654. .Where(u => u.TaskCode == taskCode && u.TenantId == tenantId)
  655. .ExecuteCommandAsync();
  656. var now = DateTime.Now;
  657. var rows = items.Where(x => !string.IsNullOrWhiteSpace(x.FormulaCode)).Select((x, i) => new MdpSyncTaskFormula
  658. {
  659. TenantId = tenantId,
  660. TaskCode = taskCode,
  661. StepCode = x.StepCode,
  662. FormulaCode = x.FormulaCode.Trim(),
  663. FormulaName = string.IsNullOrWhiteSpace(x.FormulaName) ? x.FormulaCode.Trim() : x.FormulaName.Trim(),
  664. MetricCode = x.MetricCode,
  665. FormulaExpr = x.FormulaExpr,
  666. FormulaPreview = x.FormulaPreview,
  667. Direction = string.IsNullOrWhiteSpace(x.Direction) ? "higher_is_better" : x.Direction.Trim(),
  668. YellowThreshold = x.YellowThreshold,
  669. RedThreshold = x.RedThreshold,
  670. VersionNo = x.VersionNo > 0 ? x.VersionNo : 1,
  671. IsEnabled = x.Enabled ? (byte)1 : (byte)0,
  672. SortOrder = x.SortOrder > 0 ? x.SortOrder : i + 1,
  673. Description = x.Description,
  674. CreateTime = now,
  675. UpdateTime = now
  676. }).ToList();
  677. if (rows.Count > 0)
  678. await _db.Insertable(rows).ExecuteCommandAsync();
  679. return new { ok = true, count = rows.Count };
  680. }
  681. }
  682. public class MdpSyncTaskParamSaveItem
  683. {
  684. public string? ScopeType { get; set; }
  685. public string? ScopeCode { get; set; }
  686. public string ParamKey { get; set; } = string.Empty;
  687. public string? ParamName { get; set; }
  688. public string? ParamType { get; set; }
  689. public string? ParamValue { get; set; }
  690. public string? DefaultValue { get; set; }
  691. public bool Required { get; set; }
  692. public bool Editable { get; set; } = true;
  693. public int SortOrder { get; set; }
  694. public string? Description { get; set; }
  695. }
  696. public class MdpSyncTaskFormulaSaveItem
  697. {
  698. public string? StepCode { get; set; }
  699. public string FormulaCode { get; set; } = string.Empty;
  700. public string? FormulaName { get; set; }
  701. public string? MetricCode { get; set; }
  702. public string? FormulaExpr { get; set; }
  703. public string? FormulaPreview { get; set; }
  704. public string? Direction { get; set; }
  705. public decimal? YellowThreshold { get; set; }
  706. public decimal? RedThreshold { get; set; }
  707. public int VersionNo { get; set; } = 1;
  708. public bool Enabled { get; set; } = true;
  709. public int SortOrder { get; set; }
  710. public string? Description { get; set; }
  711. }