StocktakeLabelConfirmService.cs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto;
  2. namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  3. /// <summary>
  4. /// S5 盘点标签确认 只读 list 服务。
  5. ///
  6. /// 数据源:aidopdev.MissedPrint(IsTag=1) LEFT JOIN ItemMaster(物料名称/规格/单位)。
  7. /// 参考本仓既有 MissedPrint 直读只读投影先例:LabelQueryService / StockQueryService / PendingInspectionService。
  8. ///
  9. /// 本服务仅 SELECT:无新增/编辑/删除/确认写回/批量确认/反确认/库存事务/状态流转;扁平列表,无 detail。
  10. /// MissedPrint 无 tenant_id 列,不做租户/工厂域过滤(对齐既有 MissedPrint 只读投影先例);不加 Domain 占位过滤。
  11. /// 派生列:diffQty=CompQty-Qty;status(Status='Q'→冻结);stocktakeResult(CompQty&lt;&gt;Qty→有差异/正常)。
  12. /// </summary>
  13. [ApiDescriptionSettings(Order = 308, Description = "盘点标签确认")]
  14. [Route("api/S5StocktakeLabelConfirm")]
  15. [AllowAnonymous]
  16. [NonUnify]
  17. public class StocktakeLabelConfirmService : IDynamicApiController, ITransient
  18. {
  19. private readonly ISqlSugarClient _db;
  20. public StocktakeLabelConfirmService(ISqlSugarClient db)
  21. {
  22. _db = db;
  23. }
  24. /// <summary>
  25. /// 盘点标签确认列表(只读分页查询)。
  26. /// </summary>
  27. [DisplayName("盘点标签确认列表")]
  28. [HttpGet("list")]
  29. public async Task<object> GetList([FromQuery] StocktakeLabelConfirmListInput input)
  30. {
  31. var page = input.Page <= 0 ? 1 : input.Page;
  32. var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
  33. var offset = (page - 1) * pageSize;
  34. var where = new List<string> { "m.IsTag = 1" };
  35. var pars = new List<SugarParameter>();
  36. if (!string.IsNullOrWhiteSpace(input.Location))
  37. {
  38. where.Add("m.Location LIKE @Location");
  39. pars.Add(new SugarParameter("@Location", $"%{input.Location.Trim()}%"));
  40. }
  41. if (!string.IsNullOrWhiteSpace(input.Shelf))
  42. {
  43. where.Add("m.Shelf LIKE @Shelf");
  44. pars.Add(new SugarParameter("@Shelf", $"%{input.Shelf.Trim()}%"));
  45. }
  46. if (!string.IsNullOrWhiteSpace(input.ItemNum))
  47. {
  48. where.Add("m.ItemNum LIKE @ItemNum");
  49. pars.Add(new SugarParameter("@ItemNum", $"%{input.ItemNum.Trim()}%"));
  50. }
  51. if (!string.IsNullOrWhiteSpace(input.LotSerial))
  52. {
  53. where.Add("m.LotSerial LIKE @LotSerial");
  54. pars.Add(new SugarParameter("@LotSerial", $"%{input.LotSerial.Trim()}%"));
  55. }
  56. if (string.Equals(input.InvStatus, "有差异", StringComparison.Ordinal))
  57. {
  58. where.Add("IFNULL(m.CompQty,0) <> IFNULL(m.Qty,0)");
  59. }
  60. else if (string.Equals(input.InvStatus, "正常", StringComparison.Ordinal))
  61. {
  62. where.Add("IFNULL(m.CompQty,0) = IFNULL(m.Qty,0)");
  63. }
  64. var whereSql = string.Join(" AND ", where);
  65. var total = await _db.Ado.GetIntAsync(
  66. $"SELECT COUNT(1) FROM MissedPrint m WHERE {whereSql}", pars);
  67. var list = await _db.Ado.SqlQueryAsync<StocktakeLabelConfirmListRow>(
  68. $"""
  69. SELECT
  70. m.RecID AS Id,
  71. m.ItemNum AS MaterialCode,
  72. i.Descr AS MaterialName,
  73. i.Descr1 AS Spec,
  74. m.Location AS Location,
  75. m.Shelf AS Shelf,
  76. m.FirmString2 AS ActualLocation,
  77. m.FirmString4 AS ActualShelf,
  78. m.LotSerial AS BatchNo,
  79. i.UM AS Unit,
  80. m.Qty AS LabelQty,
  81. m.CompQty AS ActualQty,
  82. IFNULL(m.CompQty,0) - IFNULL(m.Qty,0) AS DiffQty,
  83. CASE WHEN m.Status='Q' THEN '冻结' ELSE IFNULL(m.Status,'') END AS Status,
  84. CASE WHEN IFNULL(m.CompQty,0) <> IFNULL(m.Qty,0) THEN '有差异' ELSE '正常' END AS StocktakeResult,
  85. m.UpdateUser AS StocktakePerson,
  86. m.UpdateTime AS StocktakeTime,
  87. m.BarCode AS BarCode
  88. FROM MissedPrint m
  89. LEFT JOIN ItemMaster i ON i.Domain = m.Domain AND i.ItemNum = m.ItemNum
  90. WHERE {whereSql}
  91. ORDER BY {BuildOrderBy(input.OrderBy, input.OrderDir)}
  92. LIMIT {pageSize} OFFSET {offset}
  93. """,
  94. pars);
  95. return new { total, page, pageSize, list };
  96. }
  97. /// <summary>
  98. /// 库位下拉选项(MissedPrint IsTag=1 的 Location distinct,只读)。
  99. /// </summary>
  100. [DisplayName("盘点标签确认库位选项")]
  101. [HttpGet("locations")]
  102. public async Task<object> GetLocations()
  103. {
  104. var list = await _db.Ado.SqlQueryAsync<StocktakeLabelConfirmLocationOption>(
  105. """
  106. SELECT DISTINCT Location AS Val
  107. FROM MissedPrint
  108. WHERE IsTag = 1 AND Location IS NOT NULL AND Location <> ''
  109. ORDER BY Location
  110. """);
  111. return list;
  112. }
  113. /// <summary>
  114. /// 排序白名单:仅允许按已展示列排序,杜绝 SQL 注入;非法字段回落默认。
  115. /// </summary>
  116. private static string BuildOrderBy(string? orderBy, string? orderDir)
  117. {
  118. var column = orderBy switch
  119. {
  120. "itemNum" => "m.ItemNum",
  121. "location" => "m.Location",
  122. "shelf" => "m.Shelf",
  123. "lotSerial" => "m.LotSerial",
  124. "qty" => "m.Qty",
  125. "compQty" => "m.CompQty",
  126. "qtyDiff" => "(IFNULL(m.CompQty,0) - IFNULL(m.Qty,0))",
  127. "invStatus" => "(CASE WHEN IFNULL(m.CompQty,0) <> IFNULL(m.Qty,0) THEN 1 ELSE 0 END)",
  128. "updateTime" => "m.UpdateTime",
  129. _ => "m.UpdateTime",
  130. };
  131. var direction = string.Equals(orderDir, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
  132. return $"{column} {direction}, m.RecID DESC";
  133. }
  134. }