| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto;
- namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
- /// <summary>
- /// S5 盘点结果查询 只读 list 服务。
- ///
- /// 数据源:aidopdev.MissedPrint(盘点标签 IsTag=1),LEFT JOIN ItemMaster(名称/规格/单位)。
- /// 证据 sql.txt 原始口径是 <c>m.Domain=@Domain AND m.IsTag=1</c>,本实现改按租户,理由见 GetList 内注释。
- /// 盘点口径由 SQL 内定义(非脑补):
- /// 账面(标签数量)=Qty;实盘数量=CompQty;差异数量=CompQty-Qty;
- /// 盘点状态=CompQty≠Qty?'有差异':'正常';状态=Status='Q'?'冻结':'';实际库位=FirmString2;实际货架=FirmString4。
- /// 排序 UpdateTime DESC, RecID DESC。
- ///
- /// 本服务仅 SELECT:无新增/编辑/删除/库存事务/状态流转。
- /// 租户边界 = <c>MissedPrint.tenant_id</c>(正式租户列,已由生成标签写入 + 1.0.442 回填,152 行 0 NULL)。
- /// 类级已去 [AllowAnonymous]:原先该接口可被**完全未认证**调用(实测匿名 GET 返回 200),已收敛为需认证。
- /// </summary>
- [ApiDescriptionSettings(Order = 304, Description = "盘点结果查询")]
- [Route("api/S5StocktakeResult")]
- [NonUnify]
- public class StocktakeResultService : IDynamicApiController, ITransient
- {
- /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
- private const long MainTenantId = 1300000000001L;
- private readonly ISqlSugarClient _db;
- private readonly UserManager _userManager;
- public StocktakeResultService(ISqlSugarClient db, UserManager userManager)
- {
- _db = db;
- _userManager = userManager;
- }
- /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token 或超管未选目标租户 → 拒绝,无默认回退。</summary>
- private long ResolveTenantOrThrow()
- {
- var tid = _userManager.TenantId;
- if (tid <= 0)
- throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
- if (_userManager.SuperAdmin && tid == MainTenantId)
- throw Oops.Oh("超级管理员操作前必须选择目标租户");
- return tid;
- }
- /// <summary>
- /// 盘点结果查询列表(只读分页查询)。
- /// </summary>
- [DisplayName("盘点结果查询列表")]
- [HttpGet("list")]
- public async Task<object> GetList([FromQuery] StocktakeResultListInput input)
- {
- var page = input.Page <= 0 ? 1 : input.Page;
- var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
- var offset = (page - 1) * pageSize;
- var tenantId = ResolveTenantOrThrow();
- // 租户边界 = m.tenant_id,**刻意不再叠加 m.Domain = @Domain**(2026-09-02 实测 aidopdev):
- // ① MissedPrint 里每个租户只有单一 Domain 值,Domain 对租户内零过滤价值 ——
- // 要么整租户命中、要么整租户清零,不构成任何业务筛选;
- // ② 而 UAT 恰好是被清零的那一种:SourceDomainTenantResolver 经 LocationMaster 回落给出 '8010',
- // UAT 自己的 48 行 Domain 却是空串 —— tenant-only 得 48 行,tenant+Domain 得 0 行;
- // 797 则 104 → 104 无影响,是单向误杀本租户数据。
- // 结论:Domain 在本查询里既无过滤价值又会误杀,由 tenant_id 完全取代。
- // 当前 IsTag=1 全库 0 行,缺陷被数据状态掩盖,故上述判据取自放宽 IsTag 后的候选集模拟。
- var where = new List<string> { "m.tenant_id = @TenantId", "m.IsTag = 1" };
- var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
- if (!string.IsNullOrWhiteSpace(input.MaterialCode))
- {
- where.Add("m.ItemNum LIKE @MaterialCode");
- pars.Add(new SugarParameter("@MaterialCode", $"%{input.MaterialCode.Trim()}%"));
- }
- if (!string.IsNullOrWhiteSpace(input.InvStatus))
- {
- var s = input.InvStatus.Trim();
- if (s == "有差异")
- where.Add("m.CompQty <> m.Qty");
- else if (s == "正常")
- where.Add("m.CompQty = m.Qty");
- }
- var whereSql = string.Join(" AND ", where);
- var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM MissedPrint m WHERE {whereSql}", pars);
- var list = await _db.Ado.SqlQueryAsync<StocktakeResultListRow>(
- $"""
- SELECT
- m.ItemNum AS MaterialCode,
- i.Descr AS MaterialName,
- i.Descr1 AS Spec,
- m.Location AS Location,
- m.Shelf AS Shelf,
- m.FirmString2 AS ActLocation,
- m.FirmString4 AS ActShelf,
- m.LotSerial AS BatchNo,
- i.UM AS Unit,
- m.Qty AS TagQty,
- m.CompQty AS CountQty,
- (m.CompQty - m.Qty) AS QtyDiff,
- (CASE WHEN m.Status = 'Q' THEN '冻结' ELSE '' END) AS Status,
- (CASE WHEN m.CompQty <> m.Qty THEN '有差异' ELSE '正常' END) AS InvStatus,
- m.UpdateUser AS CountUser,
- m.UpdateTime AS CountTime
- FROM MissedPrint m
- -- 物料主数据按租户关联:(tenant_id, ItemNum) 唯一(0 重复组),JOIN 恒为 1:1,
- -- SELECT 行数不会超过 COUNT。而 (Domain, ItemNum) 有 391 个重复组(全部是跨租户重名,
- -- 8010 内 143 组 / 2410 内 248 组),Domain-only 关联在重名 ItemNum 上会一对多放大。
- -- 另有一项今日就已发生的错误:UAT 的 48 行 Domain 为空串,按 Domain 关联命中 0 条,
- -- 物料名称/规格/单位整列为空;按租户关联则 152/152 行都能取到(实测 old 104 → new 152)。
- LEFT JOIN ItemMaster i ON i.tenant_id = m.tenant_id AND i.ItemNum = m.ItemNum
- WHERE {whereSql}
- ORDER BY m.UpdateTime DESC, m.RecID DESC
- LIMIT {pageSize} OFFSET {offset}
- """,
- pars);
- return new { total, page, pageSize, list };
- }
- }
|