WmsBaseDataService.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. using System.Text.Json;
  2. using Admin.NET.Plugin.AiDOP.DataPlatform.Executors;
  3. using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
  4. using Microsoft.Extensions.Logging;
  5. using SqlSugar;
  6. namespace Admin.NET.Plugin.AiDOP.DataPlatform.Wms;
  7. /// <summary>
  8. /// WP7 §2:WMS 六张基础数据表只读查询 + Outbox UPSERT 回写编排。
  9. /// 不直写 165;执行侧实时量列禁止入队。
  10. /// </summary>
  11. [ApiDescriptionSettings(Order = 326, Description = "WMS基础数据维护")]
  12. [Route("api/aidop/wms-base")]
  13. [AllowAnonymous]
  14. [NonUnify]
  15. public class WmsBaseDataService : IDynamicApiController, ITransient
  16. {
  17. public const string TargetSource = "DOPDEMORQ_SQLSERVER";
  18. private static readonly HashSet<string> AllowedTables = new(StringComparer.OrdinalIgnoreCase)
  19. {
  20. "DepartmentMaster", "EmployeeMaster", "LocationMaster",
  21. "LocationShelfMaster", "EmpWorkDutyMaster", "LinePrinter"
  22. };
  23. private static readonly HashSet<string> ShelfForbiddenWriteCols = new(StringComparer.OrdinalIgnoreCase)
  24. {
  25. "QtyOnHand", "UsedVolumeCapacity", "UsedWeightCapacity", "ContainerStatus"
  26. };
  27. private static readonly Dictionary<string, string[]> BusinessKeys = new(StringComparer.OrdinalIgnoreCase)
  28. {
  29. ["DepartmentMaster"] = ["Domain", "Department"],
  30. ["EmployeeMaster"] = ["Domain", "Employee"],
  31. ["LocationMaster"] = ["Domain", "Location"],
  32. ["LocationShelfMaster"] = ["Domain", "Location", "InvShelf"],
  33. ["EmpWorkDutyMaster"] = ["Domain", "RecID"],
  34. ["LinePrinter"] = ["Domain", "Line", "BarType"]
  35. };
  36. private readonly MdpSourceScopeFactory _scopeFactory;
  37. private readonly MdpOutboxEnqueueService _enqueue;
  38. private readonly ILogger<WmsBaseDataService> _logger;
  39. public WmsBaseDataService(
  40. MdpSourceScopeFactory scopeFactory,
  41. MdpOutboxEnqueueService enqueue,
  42. ILogger<WmsBaseDataService> logger)
  43. {
  44. _scopeFactory = scopeFactory;
  45. _enqueue = enqueue;
  46. _logger = logger;
  47. }
  48. [DisplayName("基础数据分页查询")]
  49. [HttpGet("page")]
  50. public async Task<object> Page(
  51. [FromQuery] string table,
  52. [FromQuery] string? domain,
  53. [FromQuery] string? keyword,
  54. [FromQuery] int page = 1,
  55. [FromQuery] int pageSize = 50,
  56. CancellationToken ct = default)
  57. {
  58. ValidateTable(table);
  59. if (page < 1) page = 1;
  60. if (pageSize < 1 || pageSize > 200) pageSize = 50;
  61. var db = await _scopeFactory.GetScopeAsync(TargetSource, ct);
  62. var d = string.IsNullOrWhiteSpace(domain) ? "8010" : domain.Trim();
  63. var (sql, countSql, pars) = BuildListSql(table, d, keyword);
  64. var total = Convert.ToInt32(await db.Ado.GetScalarAsync(countSql, pars));
  65. var offset = (page - 1) * pageSize;
  66. var pageSql = sql + $" ORDER BY RecID OFFSET {offset} ROWS FETCH NEXT {pageSize} ROWS ONLY";
  67. var rows = await db.Ado.SqlQueryAsync<dynamic>(pageSql, pars);
  68. return new { table, domain = d, total, page, pageSize, list = rows };
  69. }
  70. [DisplayName("基础数据 UPSERT 入队")]
  71. [HttpPost("upsert")]
  72. public async Task<object> Upsert([FromBody] WmsBaseUpsertInput input, CancellationToken ct = default)
  73. {
  74. if (input == null) throw Oops.Oh("body 不能为空");
  75. ValidateTable(input.Table);
  76. if (input.Row == null || input.Row.Count == 0) throw Oops.Oh("row 不能为空");
  77. var row = NormalizeRow(input.Table, input.Row);
  78. var keys = ExtractKeys(input.Table, row);
  79. if (keys.Count == 0) throw Oops.Oh("缺少业务键");
  80. // LocationMaster.Storer 变更影响派单:记审计日志
  81. if (string.Equals(input.Table, "LocationMaster", StringComparison.OrdinalIgnoreCase)
  82. && row.TryGetValue("Storer", out var newStorer))
  83. {
  84. _logger.LogWarning(
  85. "WMS LocationMaster.Storer 变更入队 Domain={Domain} Location={Location} Storer={Storer}",
  86. keys.GetValueOrDefault("Domain"), keys.GetValueOrDefault("Location"), newStorer);
  87. }
  88. var insert = new Dictionary<string, object?>(row, StringComparer.OrdinalIgnoreCase);
  89. var update = new Dictionary<string, object?>(row, StringComparer.OrdinalIgnoreCase);
  90. // identity / 业务键不进 UPDATE SET
  91. foreach (var k in keys.Keys) update.Remove(k);
  92. update.Remove("RecID");
  93. insert.Remove("RecID"); // identity
  94. if (update.Count == 0 && insert.Count == 0)
  95. throw Oops.Oh("没有可写入列");
  96. var payload = new Dictionary<string, object?>
  97. {
  98. ["op"] = "UPSERT",
  99. ["table"] = input.Table,
  100. ["keys"] = keys,
  101. ["insert"] = insert,
  102. ["update"] = update,
  103. ["expect"] = new Dictionary<string, object?>()
  104. };
  105. var json = JsonSerializer.Serialize(payload);
  106. var keyParts = string.Join("|", keys.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
  107. .Select(kv => $"{kv.Key}={kv.Value}"));
  108. var idem = $"wmsbase|{input.Table}|{keyParts}|{StableHash(json)}";
  109. var item = new MdpOutbox
  110. {
  111. TenantId = input.TenantId,
  112. TargetSourceCode = TargetSource,
  113. ActionCode = $"WMS_BASE_{input.Table.ToUpperInvariant()}",
  114. IdemKey = idem.Length > 200 ? idem[..200] : idem,
  115. PayloadJson = json
  116. };
  117. var enqueued = await _enqueue.TryEnqueueAsync(item, ct);
  118. return new { enqueued, idemKey = item.IdemKey, table = input.Table, keys };
  119. }
  120. private static (string sql, string countSql, SugarParameter[] pars) BuildListSql(
  121. string table, string domain, string? keyword)
  122. {
  123. var pars = new List<SugarParameter> { new("@d", domain) };
  124. var where = "Domain=@d";
  125. if (!string.IsNullOrWhiteSpace(keyword))
  126. {
  127. var kw = $"%{keyword.Trim()}%";
  128. pars.Add(new SugarParameter("@kw", kw));
  129. where += table.ToLowerInvariant() switch
  130. {
  131. "departmentmaster" => " AND (Department LIKE @kw OR Descr LIKE @kw)",
  132. "employeemaster" => " AND (Employee LIKE @kw OR Name LIKE @kw OR UserName LIKE @kw)",
  133. "locationmaster" => " AND (Location LIKE @kw OR Descr LIKE @kw OR Storer LIKE @kw)",
  134. "locationshelfmaster" => " AND (Location LIKE @kw OR InvShelf LIKE @kw OR Descr LIKE @kw)",
  135. "empworkdutymaster" => " AND (Employee LIKE @kw OR ItemNum1 LIKE @kw OR ItemNum2 LIKE @kw OR Location LIKE @kw OR Duty LIKE @kw)",
  136. "lineprinter" => " AND (Line LIKE @kw OR BarType LIKE @kw OR Pinter LIKE @kw OR Storer LIKE @kw)",
  137. _ => ""
  138. };
  139. }
  140. var sql = $"SELECT * FROM [{table}] WHERE {where}";
  141. var countSql = $"SELECT COUNT(1) FROM [{table}] WHERE {where}";
  142. return (sql, countSql, pars.ToArray());
  143. }
  144. private static Dictionary<string, object?> NormalizeRow(string table, Dictionary<string, object?> row)
  145. {
  146. var result = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
  147. foreach (var kv in row)
  148. {
  149. if (string.IsNullOrWhiteSpace(kv.Key)) continue;
  150. if (string.Equals(table, "LocationShelfMaster", StringComparison.OrdinalIgnoreCase)
  151. && ShelfForbiddenWriteCols.Contains(kv.Key))
  152. continue;
  153. result[kv.Key] = UnwrapJson(kv.Value);
  154. }
  155. if (!result.ContainsKey("UpdateTime"))
  156. result["UpdateTime"] = DateTime.Now;
  157. return result;
  158. }
  159. private static Dictionary<string, object?> ExtractKeys(string table, Dictionary<string, object?> row)
  160. {
  161. var keys = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
  162. var isDuty = string.Equals(table, "EmpWorkDutyMaster", StringComparison.OrdinalIgnoreCase);
  163. var recId = ReadInt64(row, "RecID");
  164. var dutyIsNew = isDuty && (recId == null || recId <= 0);
  165. foreach (var k in BusinessKeys[table])
  166. {
  167. if (dutyIsNew && string.Equals(k, "RecID", StringComparison.OrdinalIgnoreCase))
  168. continue;
  169. if (!row.TryGetValue(k, out var v) || v == null || (v is string s && string.IsNullOrWhiteSpace(s)))
  170. throw Oops.Oh($"缺少业务键 {k}");
  171. keys[k] = v;
  172. }
  173. if (dutyIsNew)
  174. {
  175. // 新建行:自然键做 EXISTS;INSERT 不含 RecID(identity)
  176. foreach (var nk in new[] { "Domain", "Employee", "ItemNum1", "ItemNum2", "Location", "DutyType" })
  177. {
  178. if (!row.TryGetValue(nk, out var v) || v == null || (v is string s && string.IsNullOrWhiteSpace(s)))
  179. throw Oops.Oh($"EmpWorkDutyMaster 新建缺少自然键 {nk}");
  180. keys[nk] = v;
  181. }
  182. }
  183. return keys;
  184. }
  185. private static object? UnwrapJson(object? v)
  186. {
  187. if (v is JsonElement je)
  188. {
  189. return je.ValueKind switch
  190. {
  191. JsonValueKind.Null or JsonValueKind.Undefined => null,
  192. JsonValueKind.String => je.GetString(),
  193. JsonValueKind.True => true,
  194. JsonValueKind.False => false,
  195. JsonValueKind.Number => je.TryGetInt64(out var l) ? l : je.GetDecimal(),
  196. _ => je.GetRawText()
  197. };
  198. }
  199. return v;
  200. }
  201. private static long? ReadInt64(Dictionary<string, object?> row, string key)
  202. {
  203. if (!row.TryGetValue(key, out var v) || v == null) return null;
  204. v = UnwrapJson(v);
  205. return v switch
  206. {
  207. long l => l,
  208. int i => i,
  209. short s => s,
  210. decimal d => (long)d,
  211. string str when long.TryParse(str, out var p) => p,
  212. _ => null
  213. };
  214. }
  215. private static void ValidateTable(string? table)
  216. {
  217. if (string.IsNullOrWhiteSpace(table) || !AllowedTables.Contains(table))
  218. throw Oops.Oh($"不支持的表:{table}");
  219. }
  220. private static string StableHash(string s)
  221. {
  222. var hash = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(s));
  223. return Convert.ToHexString(hash.AsSpan(0, 8));
  224. }
  225. }
  226. public sealed class WmsBaseUpsertInput
  227. {
  228. public string Table { get; set; } = "";
  229. public long TenantId { get; set; }
  230. public Dictionary<string, object?> Row { get; set; } = new(StringComparer.OrdinalIgnoreCase);
  231. }