StocktakeResultService.cs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  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),忠实复刻证据 sql.txt:
  7. /// LEFT JOIN ItemMaster(名称/规格/单位),过滤 m.Domain=@Domain AND m.IsTag=1。
  8. /// 盘点口径由 SQL 内定义(非脑补):
  9. /// 账面(标签数量)=Qty;实盘数量=CompQty;差异数量=CompQty-Qty;
  10. /// 盘点状态=CompQty≠Qty?'有差异':'正常';状态=Status='Q'?'冻结':'';实际库位=FirmString2;实际货架=FirmString4。
  11. /// Domain 按当前租户经 SourceDomainTenantResolver 解析(不再全局取第一条、无 8010 回落)。排序 UpdateTime DESC, RecID DESC。
  12. ///
  13. /// 本服务仅 SELECT:无新增/编辑/删除/库存事务/状态流转。
  14. /// MissedPrint 的 tenant_id 列存在但实测全为 NULL,无法做租户行级过滤,仅能按 Domain 公司域收敛(既有数据模型约束)。
  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. /// <summary>贴源源码:与 S5 其余库存页保持一致,供 Domain 解析使用。</summary>
  25. private const string SourceCode = "DOPDEMORQ_SQLSERVER";
  26. private readonly ISqlSugarClient _db;
  27. private readonly UserManager _userManager;
  28. private readonly SourceDomainTenantResolver _domainTenant;
  29. public StocktakeResultService(
  30. ISqlSugarClient db, UserManager userManager, SourceDomainTenantResolver domainTenant)
  31. {
  32. _db = db;
  33. _userManager = userManager;
  34. _domainTenant = domainTenant;
  35. }
  36. /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token 或超管未选目标租户 → 拒绝,无默认回退。</summary>
  37. private long ResolveTenantOrThrow()
  38. {
  39. var tid = _userManager.TenantId;
  40. if (tid <= 0)
  41. throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
  42. if (_userManager.SuperAdmin && tid == MainTenantId)
  43. throw Oops.Oh("超级管理员操作前必须选择目标租户");
  44. return tid;
  45. }
  46. /// <summary>
  47. /// 按当前租户解析公司域,统一走 <see cref="SourceDomainTenantResolver"/>。
  48. /// <para>
  49. /// 原实现是 <c>SELECT Domain FROM GeneralizedCodeMaster WHERE FldName='SystemConfig'
  50. /// AND Val='CompanyCode' ORDER BY RecID LIMIT 1</c> —— **无 tenant 条件**,取全库第一条并硬回落 "8010",
  51. /// 与 resolver 修复前的缺陷同源。本项目的 Domain 解析只允许有一个正式实现。
  52. /// </para>
  53. /// </summary>
  54. private async Task<string> ResolveDomainOrThrowAsync(long tenantId, CancellationToken ct = default)
  55. {
  56. try
  57. {
  58. return await _domainTenant.ResolveDomainAsync(SourceCode, tenantId, ct);
  59. }
  60. catch (Exception ex)
  61. {
  62. throw Oops.Oh($"无法解析当前租户 Domain 映射:{ex.Message}");
  63. }
  64. }
  65. /// <summary>
  66. /// 盘点结果查询列表(只读分页查询)。
  67. /// </summary>
  68. [DisplayName("盘点结果查询列表")]
  69. [HttpGet("list")]
  70. public async Task<object> GetList([FromQuery] StocktakeResultListInput input)
  71. {
  72. var page = input.Page <= 0 ? 1 : input.Page;
  73. var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
  74. var offset = (page - 1) * pageSize;
  75. var domain = await ResolveDomainOrThrowAsync(ResolveTenantOrThrow());
  76. var where = new List<string> { "m.Domain = @Domain", "m.IsTag = 1" };
  77. var pars = new List<SugarParameter> { new("@Domain", domain) };
  78. if (!string.IsNullOrWhiteSpace(input.MaterialCode))
  79. {
  80. where.Add("m.ItemNum LIKE @MaterialCode");
  81. pars.Add(new SugarParameter("@MaterialCode", $"%{input.MaterialCode.Trim()}%"));
  82. }
  83. if (!string.IsNullOrWhiteSpace(input.InvStatus))
  84. {
  85. var s = input.InvStatus.Trim();
  86. if (s == "有差异")
  87. where.Add("m.CompQty <> m.Qty");
  88. else if (s == "正常")
  89. where.Add("m.CompQty = m.Qty");
  90. }
  91. var whereSql = string.Join(" AND ", where);
  92. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM MissedPrint m WHERE {whereSql}", pars);
  93. var list = await _db.Ado.SqlQueryAsync<StocktakeResultListRow>(
  94. $"""
  95. SELECT
  96. m.ItemNum AS MaterialCode,
  97. i.Descr AS MaterialName,
  98. i.Descr1 AS Spec,
  99. m.Location AS Location,
  100. m.Shelf AS Shelf,
  101. m.FirmString2 AS ActLocation,
  102. m.FirmString4 AS ActShelf,
  103. m.LotSerial AS BatchNo,
  104. i.UM AS Unit,
  105. m.Qty AS TagQty,
  106. m.CompQty AS CountQty,
  107. (m.CompQty - m.Qty) AS QtyDiff,
  108. (CASE WHEN m.Status = 'Q' THEN '冻结' ELSE '' END) AS Status,
  109. (CASE WHEN m.CompQty <> m.Qty THEN '有差异' ELSE '正常' END) AS InvStatus,
  110. m.UpdateUser AS CountUser,
  111. m.UpdateTime AS CountTime
  112. FROM MissedPrint m
  113. LEFT JOIN ItemMaster i ON m.Domain = i.Domain AND m.ItemNum = i.ItemNum
  114. WHERE {whereSql}
  115. ORDER BY m.UpdateTime DESC, m.RecID DESC
  116. LIMIT {pageSize} OFFSET {offset}
  117. """,
  118. pars);
  119. return new { total, page, pageSize, list };
  120. }
  121. }