StocktakeResultService.cs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  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. /// 证据 sql.txt 原始口径是 <c>m.Domain=@Domain AND m.IsTag=1</c>,本实现改按租户,理由见 GetList 内注释。
  8. /// 盘点口径由 SQL 内定义(非脑补):
  9. /// 账面(标签数量)=Qty;实盘数量=CompQty;差异数量=CompQty-Qty;
  10. /// 盘点状态=CompQty≠Qty?'有差异':'正常';状态=Status='Q'?'冻结':'';实际库位=FirmString2;实际货架=FirmString4。
  11. /// 排序 UpdateTime DESC, RecID DESC。
  12. ///
  13. /// 本服务仅 SELECT:无新增/编辑/删除/库存事务/状态流转。
  14. /// 租户边界 = <c>MissedPrint.tenant_id</c>(正式租户列,已由生成标签写入 + 1.0.442 回填,152 行 0 NULL)。
  15. /// 类级已去 [AllowAnonymous]:原先该接口可被**完全未认证**调用(实测匿名 GET 返回 200),已收敛为需认证。
  16. /// </summary>
  17. [ApiDescriptionSettings(Order = 304, Description = "盘点结果查询")]
  18. [Route("api/S5StocktakeResult")]
  19. [NonUnify]
  20. public class StocktakeResultService : IDynamicApiController, ITransient
  21. {
  22. /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
  23. private const long MainTenantId = 1300000000001L;
  24. private readonly ISqlSugarClient _db;
  25. private readonly UserManager _userManager;
  26. public StocktakeResultService(ISqlSugarClient db, UserManager userManager)
  27. {
  28. _db = db;
  29. _userManager = userManager;
  30. }
  31. /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token 或超管未选目标租户 → 拒绝,无默认回退。</summary>
  32. private long ResolveTenantOrThrow()
  33. {
  34. var tid = _userManager.TenantId;
  35. if (tid <= 0)
  36. throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
  37. if (_userManager.SuperAdmin && tid == MainTenantId)
  38. throw Oops.Oh("超级管理员操作前必须选择目标租户");
  39. return tid;
  40. }
  41. /// <summary>
  42. /// 盘点结果查询列表(只读分页查询)。
  43. /// </summary>
  44. [DisplayName("盘点结果查询列表")]
  45. [HttpGet("list")]
  46. public async Task<object> GetList([FromQuery] StocktakeResultListInput input)
  47. {
  48. var page = input.Page <= 0 ? 1 : input.Page;
  49. var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
  50. var offset = (page - 1) * pageSize;
  51. var tenantId = ResolveTenantOrThrow();
  52. // 租户边界 = m.tenant_id,**刻意不再叠加 m.Domain = @Domain**(2026-09-02 实测 aidopdev):
  53. // ① MissedPrint 里每个租户只有单一 Domain 值,Domain 对租户内零过滤价值 ——
  54. // 要么整租户命中、要么整租户清零,不构成任何业务筛选;
  55. // ② 而 UAT 恰好是被清零的那一种:SourceDomainTenantResolver 经 LocationMaster 回落给出 '8010',
  56. // UAT 自己的 48 行 Domain 却是空串 —— tenant-only 得 48 行,tenant+Domain 得 0 行;
  57. // 797 则 104 → 104 无影响,是单向误杀本租户数据。
  58. // 结论:Domain 在本查询里既无过滤价值又会误杀,由 tenant_id 完全取代。
  59. // 当前 IsTag=1 全库 0 行,缺陷被数据状态掩盖,故上述判据取自放宽 IsTag 后的候选集模拟。
  60. var where = new List<string> { "m.tenant_id = @TenantId", "m.IsTag = 1" };
  61. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  62. if (!string.IsNullOrWhiteSpace(input.MaterialCode))
  63. {
  64. where.Add("m.ItemNum LIKE @MaterialCode");
  65. pars.Add(new SugarParameter("@MaterialCode", $"%{input.MaterialCode.Trim()}%"));
  66. }
  67. if (!string.IsNullOrWhiteSpace(input.InvStatus))
  68. {
  69. var s = input.InvStatus.Trim();
  70. if (s == "有差异")
  71. where.Add("m.CompQty <> m.Qty");
  72. else if (s == "正常")
  73. where.Add("m.CompQty = m.Qty");
  74. }
  75. var whereSql = string.Join(" AND ", where);
  76. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM MissedPrint m WHERE {whereSql}", pars);
  77. var list = await _db.Ado.SqlQueryAsync<StocktakeResultListRow>(
  78. $"""
  79. SELECT
  80. m.ItemNum AS MaterialCode,
  81. i.Descr AS MaterialName,
  82. i.Descr1 AS Spec,
  83. m.Location AS Location,
  84. m.Shelf AS Shelf,
  85. m.FirmString2 AS ActLocation,
  86. m.FirmString4 AS ActShelf,
  87. m.LotSerial AS BatchNo,
  88. i.UM AS Unit,
  89. m.Qty AS TagQty,
  90. m.CompQty AS CountQty,
  91. (m.CompQty - m.Qty) AS QtyDiff,
  92. (CASE WHEN m.Status = 'Q' THEN '冻结' ELSE '' END) AS Status,
  93. (CASE WHEN m.CompQty <> m.Qty THEN '有差异' ELSE '正常' END) AS InvStatus,
  94. m.UpdateUser AS CountUser,
  95. m.UpdateTime AS CountTime
  96. FROM MissedPrint m
  97. -- 物料主数据按租户关联:(tenant_id, ItemNum) 唯一(0 重复组),JOIN 恒为 1:1,
  98. -- SELECT 行数不会超过 COUNT。而 (Domain, ItemNum) 有 391 个重复组(全部是跨租户重名,
  99. -- 8010 内 143 组 / 2410 内 248 组),Domain-only 关联在重名 ItemNum 上会一对多放大。
  100. -- 另有一项今日就已发生的错误:UAT 的 48 行 Domain 为空串,按 Domain 关联命中 0 条,
  101. -- 物料名称/规格/单位整列为空;按租户关联则 152/152 行都能取到(实测 old 104 → new 152)。
  102. LEFT JOIN ItemMaster i ON i.tenant_id = m.tenant_id AND i.ItemNum = m.ItemNum
  103. WHERE {whereSql}
  104. ORDER BY m.UpdateTime DESC, m.RecID DESC
  105. LIMIT {pageSize} OFFSET {offset}
  106. """,
  107. pars);
  108. return new { total, page, pageSize, list };
  109. }
  110. }